use crate::DbPool; use crate::providers::Provider; use crate::schema::videos; use crate::util::cache::VideoCache; use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr}; use crate::util::parse_abbreviated_number; use crate::util::time::parse_time_to_seconds; use crate::videos::VideoItem; use cute::c; use error_chain::error_chain; use htmlentity::entity::{ICodedDataTrait, decode}; use std::env; use std::vec; use wreq::{Client, Proxy}; use wreq_util::Emulation; #[macro_use(c)] error_chain! { foreign_links { Io(std::io::Error); HttpRequest(wreq::Error); } } #[derive(serde::Serialize)] struct PmvhavenRequest { all: bool, //true, pmv: bool, //false, hmv: bool, //false, hypno: bool, //false, tiktok: bool, //false, koreanbj: bool, //false, other: bool, // false, explicitContent: Option, //null, sameSexContent: Option, //null, seizureWarning: Option, //null, tags: Vec, //[], music: Vec, //[], stars: Vec, //[], creators: Vec, //[], range: Vec, //[0,40], activeTime: String, //"All time", activeQuality: String, //"Quality", aspectRatio: String, //"Aspect Ratio", activeView: String, //"Newest", index: u32, //2, hideUntagged: bool, //true, showSubscriptionsOnly: bool, //false, query: String, //"no", profile: Option, //null } impl PmvhavenRequest { pub fn new(page: u32) -> Self { PmvhavenRequest { all: true, pmv: false, hmv: false, hypno: false, tiktok: false, koreanbj: false, other: false, explicitContent: None, sameSexContent: None, seizureWarning: None, tags: vec![], music: vec![], stars: vec![], creators: vec![], range: vec![0, 40], activeTime: "All time".to_string(), activeQuality: "Quality".to_string(), aspectRatio: "Aspect Ratio".to_string(), activeView: "Newest".to_string(), index: page, hideUntagged: true, showSubscriptionsOnly: false, query: "no".to_string(), profile: None, } } fn hypno(&mut self) -> &mut Self { self.all = false; self.pmv = false; self.hmv = false; self.tiktok = false; self.koreanbj = false; self.other = false; self.hypno = true; self } fn pmv(&mut self) -> &mut Self { self.all = false; self.pmv = true; self.hmv = false; self.tiktok = false; self.koreanbj = false; self.other = false; self.hypno = false; self } fn hmv(&mut self) -> &mut Self { self.all = false; self.pmv = false; self.hmv = true; self.tiktok = false; self.koreanbj = false; self.other = false; self.hypno = false; self } fn tiktok(&mut self) -> &mut Self { self.all = false; self.pmv = false; self.hmv = false; self.tiktok = true; self.koreanbj = false; self.other = false; self.hypno = false; self } fn koreanbj(&mut self) -> &mut Self { self.all = false; self.pmv = false; self.hmv = false; self.tiktok = false; self.koreanbj = true; self.other = false; self.hypno = false; self } fn other(&mut self) -> &mut Self { self.all = false; self.pmv = false; self.hmv = false; self.tiktok = false; self.koreanbj = false; self.other = true; self.hypno = false; self } } #[derive(serde::Serialize)] struct PmvhavenSearch { mode: String, //"DefaultMoreSearch", data: String, //"pmv", index: u32, } impl PmvhavenSearch { fn new(search: String, page: u32) -> PmvhavenSearch { PmvhavenSearch { mode: "DefaultMoreSearch".to_string(), data: search, index: page, } } } #[derive(serde::Deserialize)] struct PmvhavenVideo { title: String, //JAV Addiction Therapy", uploader: Option, //itonlygetsworse", duration: f32, //259.093333, width: Option, //3840", height: Option, //2160", ratio: Option, //50, thumbnails: Vec>, //[ // "placeholder", // "https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/thumbnail/JAV Addiction Therapy_686f24e96f7124f3dfbe90ab.png", // "https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/thumbnail/webp320_686f24e96f7124f3dfbe90ab.webp" // ], views: u32, //1971, url: Option, //https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/JAV Addiction Therapy_686f24e96f7124f3dfbe90ab.mp4", previewUrlCompressed: Option, //https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/videoPreview/comus_686f24e96f7124f3dfbe90ab.mp4", seizureWarning: Option, //false, isoDate: Option, //2025-07-10T02:52:26.000Z", gayContent: Option, //false, transContent: Option, //false, creator: Option, //itonlygetsworse", _id: String, //686f2aeade2062f93d72931f", totalRaters: Option, //42, rating: Option, //164 } impl PmvhavenVideo { fn to_videoitem(self) -> VideoItem { let mut item = VideoItem::new( self._id.clone(), self.title.clone(), format!("https://pmvhaven.com/video/{}_{}", self.title.replace(" ","-"), self._id), "pmvhaven".to_string(), self.thumbnails[self.thumbnails.len()-1].clone().unwrap_or("".to_string()), self.duration as u32, ) .views(self.views); item = match self.creator{ Some(c) => item.uploader(c), _ => item, }; item = match self.previewUrlCompressed{ Some(u) => item.preview(u), _ => item, }; return item; } } #[derive(serde::Deserialize)] struct PmvhavenResponse { httpStatusCode: Option, data: Vec, count: Option, } impl PmvhavenResponse { fn to_videoitems(self) -> Vec { return c![video.to_videoitem(), for video in self.data]; } } #[derive(Debug, Clone)] pub struct PmvhavenProvider { url: String, } impl PmvhavenProvider { pub fn new() -> Self { PmvhavenProvider { url: "https://pmvhaven.com".to_string(), } } async fn get(&self, cache: VideoCache, page: u8, category: String) -> Result> { let url = format!("{}/api/getmorevideos", self.url); let request = PmvhavenRequest::new(page as u32); let old_items = match cache.get(&url) { Some((time, items)) => { if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 { println!("Cache hit for URL: {}", url); return Ok(items.clone()); } else { 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 response = client .post(url.clone()) .proxy(proxy) .json(&request) .header("Content-Type", "text/plain;charset=UTF-8") .send() .await?; if response.status().is_success() { let videos = match response.json::().await { Ok(resp) => resp, Err(e) => { println!("Failed to parse PmvhavenResponse: {}", e); return Ok(old_items); } }; let video_items: Vec = videos.to_videoitems(); if !video_items.is_empty() { cache.remove(&url); cache.insert(url.clone(), video_items.clone()); } else { return Ok(old_items); } return 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) // } Err("Failed to get Videos".into()) } async fn query(&self, cache: VideoCache, page: u8, query: &str) -> Result> { let url = format!("{}/api/v2/search", self.url); let request = PmvhavenSearch::new(query.to_string(),page as u32); // 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 * 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 response = client .post(url.clone()) .proxy(proxy) .json(&request) .header("Content-Type", "application/json") .header("Accept", "application/json") .send() .await?; if response.status().is_success() { let videos = match response.json::().await { Ok(resp) => resp, Err(e) => { println!("Failed to parse PmvhavenResponse: {}", e); return Ok(old_items); } }; let video_items: Vec = videos.to_videoitems(); if !video_items.is_empty() { cache.remove(&url); cache.insert(url.clone(), video_items.clone()); } else { return Ok(old_items); } return 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) // } Err("Failed to query Videos".into()) } } impl Provider for PmvhavenProvider { async fn get_videos( &self, cache: VideoCache, pool: DbPool, _channel: String, sort: String, query: Option, page: String, per_page: String, featured: String, category: 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), category) .await } }; match videos { Ok(v) => v, Err(e) => { println!("Error fetching videos: {}", e); vec![] } } } }