From 5522f2e37d79464a941d1648bb96ae59fc51fc43 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 29 Nov 2025 17:16:21 +0000 Subject: [PATCH] pmvhaven backend fix --- src/api.rs | 79 +----- src/providers/mod.rs | 1 + src/providers/pmvhaven.rs | 512 +++++++++++++++----------------------- src/videos.rs | 1 + 4 files changed, 202 insertions(+), 391 deletions(-) diff --git a/src/api.rs b/src/api.rs index 564413b..8755aae 100644 --- a/src/api.rs +++ b/src/api.rs @@ -10,7 +10,6 @@ use crate::providers::all::AllProvider; use crate::providers::hanime::HanimeProvider; use crate::providers::okporn::OkpornProvider; use crate::providers::perverzija::PerverzijaProvider; -use crate::providers::pmvhaven::PmvhavenProvider; use crate::providers::pornhub::PornhubProvider; use crate::providers::redtube::RedtubeProvider; use crate::providers::rule34video::Rule34videoProvider; @@ -164,82 +163,6 @@ async fn status(req: HttpRequest) -> Result { nsfw: true, cacheDuration: Some(1800), }); - if clientversion >= ClientVersion::new(22, 101, "22e".to_string()) { - // pmvhaven - status.add_channel(Channel { - id: "pmvhaven".to_string(), - name: "Pmvhaven".to_string(), - description: "Explore a curated collection of captivating PMV".to_string(), - premium: false, - favicon: "https://www.google.com/s2/favicons?sz=64&domain=pmvhaven.com".to_string(), - status: "active".to_string(), - categories: vec![], - options: vec![ - ChannelOption { - id: "category".to_string(), - title: "Category".to_string(), - description: "Category of PMV Video get".to_string(), //"Sort the videos by Date or Name.".to_string(), - systemImage: "folder".to_string(), - colorName: "yellow".to_string(), - options: vec![ - FilterOption { - id: "all".to_string(), - title: "All".to_string(), - }, - FilterOption { - id: "pmv".to_string(), - title: "PMV".to_string(), - }, - FilterOption { - id: "hmv".to_string(), - title: "HMV".to_string(), - }, - FilterOption { - id: "tiktok".to_string(), - title: "Tiktok".to_string(), - }, - FilterOption { - id: "koreanbj".to_string(), - title: "KoreanBJ".to_string(), - }, - FilterOption { - id: "hypno".to_string(), - title: "Hypno".to_string(), - }, - FilterOption { - id: "other".to_string(), - title: "Other".to_string(), - }, - ], - multiSelect: false, - }, - ChannelOption { - id: "sort".to_string(), - title: "Filter".to_string(), - description: "Filter PMV Videos".to_string(), - systemImage: "list.number".to_string(), - colorName: "blue".to_string(), - options: vec![ - FilterOption { - id: "Newest".to_string(), - title: "Newest".to_string(), - }, - FilterOption { - id: "Top Rated".to_string(), - title: "Top Rated".to_string(), - }, - FilterOption { - id: "Most Viewed".to_string(), - title: "Most Viewed".to_string(), - }, - ], - multiSelect: false, - }, - ], - nsfw: true, - cacheDuration: Some(1800), - }); - } if clientversion >= ClientVersion::new(22, 97, "22a".to_string()) { // perverzija status.add_channel(Channel { @@ -1241,6 +1164,7 @@ async fn videos_post( stars: Some(stars), categories: Some(categories), duration: Some(duration), + sort: Some(sort.clone()) }; let video_items = provider .get_videos( @@ -1308,7 +1232,6 @@ pub fn get_provider(channel: &str) -> Option { "perverzija" => Some(Arc::new(PerverzijaProvider::new())), "hanime" => Some(Arc::new(HanimeProvider::new())), "pornhub" => Some(Arc::new(PornhubProvider::new())), - "pmvhaven" => Some(Arc::new(PmvhavenProvider::new())), "rule34video" => Some(Arc::new(Rule34videoProvider::new())), "redtube" => Some(Arc::new(RedtubeProvider::new())), "okporn" => Some(Arc::new(OkpornProvider::new())), diff --git a/src/providers/mod.rs b/src/providers/mod.rs index a1137c3..7addd5a 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -50,6 +50,7 @@ pub static ALL_PROVIDERS: Lazy> = Lazy::new(| m.insert("rule34gen", Arc::new(rule34gen::Rule34genProvider::new()) as DynProvider); m.insert("xxdbx", Arc::new(xxdbx::XxdbxProvider::new()) as DynProvider); m.insert("hqporner", Arc::new(hqporner::HqpornerProvider::new()) as DynProvider); + m.insert("pmvhaven", Arc::new(pmvhaven::PmvhavenProvider::new()) as DynProvider); // add more here as you migrate them m }); diff --git a/src/providers/pmvhaven.rs b/src/providers/pmvhaven.rs index ce07fad..3380592 100644 --- a/src/providers/pmvhaven.rs +++ b/src/providers/pmvhaven.rs @@ -1,12 +1,15 @@ use crate::DbPool; +use crate::api::ClientVersion; use crate::providers::Provider; +use crate::status::*; use crate::util::cache::VideoCache; +use crate::util::time::parse_time_to_seconds; use crate::videos::{ServerOptions, VideoItem}; use async_trait::async_trait; -use cute::c; use error_chain::error_chain; -// use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; -use std::vec; +use htmlentity::entity::{ICodedDataTrait, decode}; +use std::sync::{Arc, RwLock}; +use std::{vec}; error_chain! { foreign_links { @@ -15,294 +18,112 @@ error_chain! { } } -#[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, - transContent: 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, - 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, - transContent: 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, - 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 encoded_title = percent_encode_emojis(&self.title); - let thumbnail = self.thumbnails[self.thumbnails.len() - 1] - .clone() - .unwrap_or("".to_string()); - // let video_id = thumbnail.split("_").collect::>().last().unwrap_or(&"").to_string().split('.').next().unwrap_or("").to_string(); - let mut item = VideoItem::new( - self._id.clone(), - self.title.clone(), - format!( - "https://pmvhaven.com/video/{}_{}", - self.title.replace(" ", "-"), - self._id - ), - "pmvhaven".to_string(), - thumbnail, - 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 { - 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, + stars: Arc>>, + categories: Arc>>, } impl PmvhavenProvider { pub fn new() -> Self { - PmvhavenProvider { + let provider = PmvhavenProvider { url: "https://pmvhaven.com".to_string(), + stars: Arc::new(RwLock::new(vec![])), + categories: Arc::new(RwLock::new(vec![])), + }; + provider + } + + fn build_channel(&self, clientversion: ClientVersion) -> Channel { + // if clientversion >= ClientVersion::new(22, 101, "22e".to_string()) { + let _ = clientversion; + Channel { + id: "pmvhaven".to_string(), + name: "PMVHaven".to_string(), + description: "Best PMV Videos".to_string(), + premium: false, + favicon: "https://www.google.com/s2/favicons?sz=64&domain=pmvhaven.com".to_string(), + status: "active".to_string(), + categories: self.categories.read().unwrap().iter().map(|c| c.clone()).collect(), + 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: "relevance".into(), + title: "Relevance".into(), + }, + FilterOption { + id: "newest".into(), + title: "Newest".into(), + }, + FilterOption { + id: "oldest".into(), + title: "Oldest".into(), + }, + FilterOption { + id: "most viewed".into(), + title: "Most Viewed".into(), + }, + FilterOption { + id: "most liked".into(), + title: "Most Liked".into(), + }, + FilterOption { + id: "most disliked".into(), + title: "Most Disliked".into(), + }, + ], + multiSelect: false, + }, + ChannelOption { + id: "duration".to_string(), + title: "Duration".to_string(), + description: "Length of the Videos".to_string(), + systemImage: "timer".to_string(), + colorName: "green".to_string(), + options: vec![ + FilterOption { + id: "any".into(), + title: "Any".into(), + }, + FilterOption { + id: "<4 min".into(), + title: "<4 min".into(), + }, + FilterOption { + id: "4-20 min".into(), + title: "4-20 min".into(), + }, + FilterOption { + id: "20-60 min".into(), + title: "20-60 min".into(), + }, + FilterOption { + id: ">1 hour".into(), + title: ">1 hour".into(), + }, + ], + multiSelect: false, + }, + ], + nsfw: true, + cacheDuration: None, } } - async fn get( - &self, - cache: VideoCache, - page: u8, - sort: String, - options: ServerOptions, - ) -> Result> { - let category = options.category.unwrap_or("".to_string()); - let index = format!("pmvhaven:{}:{}", page, category); - let url = format!("{}/api/getmorevideos", self.url); - let mut request = PmvhavenRequest::new(page as u32); - request.activeView = sort; - request = match category.as_str() { - "hypno" => { - request.hypno(); - request - } - "pmv" => { - request.pmv(); - request - } - "hmv" => { - request.hmv(); - request - } - "tiktok" => { - request.tiktok(); - request - } - "koreanbj" => { - request.koreanbj(); - request - } - "other" => { - request.other(); - request - } - _ => request, - }; - let old_items = match cache.get(&index) { - 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() - } + // Push one item with minimal lock time and dedup by id + fn push_unique(target: &Arc>>, item: String) { + if let Ok(mut vec) = target.write() { + if !vec.iter().any(|x| x == &item) { + println!("Added new item: {}", item.clone()); + vec.push(item); } - None => { - vec![] - } - }; - - let mut requester = options.requester.clone().unwrap(); - let response = requester.post(&url, &request, vec![("Content-Type".to_string(),"text/plain;charset=UTF-8".to_string())]).await.unwrap(); - 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); } async fn query( @@ -312,11 +133,34 @@ impl PmvhavenProvider { query: &str, options: ServerOptions, ) -> Result> { - let index = format!("pmvhaven:{}:{}", query, page); - 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(&index) { + let search_string = query.trim().to_string(); + let sort_string = match options.sort.unwrap_or("".to_string()).as_str() { + "newest" => "sort=-uploadDate", + "oldest" => "sort=uploadDate", + "most viewed" => "sort=-views", + "most liked" => "sort=-likes", + "most disliked" => "sort=-dislikes", + _ => "", + }; + let duration_string = match options.duration.unwrap_or("".to_string()).as_str(){ + "<4 min" => "durationMax=240", + "4-20 min" => "durationMin=240&durationMax=1200", + "20-60 min" => "durationMin=1200&durationMax=3600", + ">1 hour" => "durationMin=3600", + _ => "", + }; + let endpoint = if search_string.is_empty() { + "api/videos" + } else { + "api/videos/search" + }; + let mut video_url = format!("{}/{}?limit=100&page={}&{}&{}", self.url, endpoint, page, duration_string, sort_string); + if let Some(star) = self.stars.read().unwrap().iter().find(|s| s.to_ascii_lowercase() == search_string.to_ascii_lowercase()) { + video_url = format!("{}&stars={}", video_url, star); + } else if let Some(category) = self.categories.read().unwrap().iter().find(|s| s.to_ascii_lowercase() == search_string.to_ascii_lowercase()) { + video_url = format!("{}&tagMode=AND&tags={}", video_url, category); + } + 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()); @@ -331,23 +175,75 @@ impl PmvhavenProvider { }; let mut requester = options.requester.clone().unwrap(); - let response = requester.post(&url, &request, vec![("Content-Type".to_string(),"text/plain;charset=UTF-8".to_string())]).await.unwrap(); - 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(); + + let text = requester.get(&video_url).await.unwrap(); + let json = serde_json::from_str::(&text).unwrap_or(serde_json::Value::Null); + let video_items: Vec = self + .get_video_items_from_json(json) + .await; if !video_items.is_empty() { - cache.remove(&url); - cache.insert(url.clone(), video_items.clone()); + cache.remove(&video_url); + cache.insert(video_url.clone(), video_items.clone()); } else { return Ok(old_items); } - return Ok(video_items); + Ok(video_items) } + + async fn get_video_items_from_json( + &self, + json: serde_json::Value, + ) -> Vec { + if json.is_null() { + return vec![]; + } + let mut items = vec![]; + let success = json["success"].as_bool().unwrap_or(false); + if !success { + return items; + } + let videos = json["data"].as_array().cloned().unwrap_or_default(); + if videos.is_empty() { + return items; + } + for video in videos.clone() { + let title = decode(video["title"].as_str().unwrap_or("").as_bytes()).to_string().unwrap_or("".to_string()); + let id = video["_id"].as_str().unwrap_or(title.clone().as_str()).to_string(); + let video_url = video["videoUrl"].as_str().unwrap_or("").to_string(); + let views = video["views"].as_u64().unwrap_or(0); + let thumb = video["thumbnailUrl"].as_str().unwrap_or("").to_string(); + let duration_str = video["duration"].as_str().unwrap_or("0"); + let duration = parse_time_to_seconds(duration_str).unwrap_or(0); + let preview = video["previewUrl"].as_str().unwrap_or("").to_string(); + let tags_array = video["tags"].as_array().cloned().unwrap_or_default(); + for tag in tags_array.clone() { + let tag_str = decode(tag.as_str().unwrap_or("").as_bytes()).to_string().unwrap_or("".to_string()); + Self::push_unique(&self.categories, tag_str.clone()); + } + let stars_array = video["starsTags"].as_array().cloned().unwrap_or_default(); + for tag in stars_array.clone() { + let tag_str = decode(tag.as_str().unwrap_or("").as_bytes()).to_string().unwrap_or("".to_string()); + Self::push_unique(&self.stars, tag_str.clone()); + } + + let tags = stars_array.iter().chain(tags_array.iter()).cloned().collect::>(); + let video_item = VideoItem::new( + id, + title, + video_url.to_string(), + "pmvhaven".to_string(), + thumb, + duration as u32, + ) + .views(views as u32) + .preview(preview) + .tags(tags.iter().map(|t| decode(t.as_str().unwrap_or("").as_bytes()).to_string().unwrap_or("".to_string())).collect()); + items.push(video_item); + } + + + return items; + } } #[async_trait] @@ -356,29 +252,16 @@ impl Provider for PmvhavenProvider { &self, cache: VideoCache, pool: DbPool, - sort: String, + _sort: String, query: Option, page: String, per_page: String, options: ServerOptions, ) -> Vec { let _ = per_page; - 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, options) - .await - } - None => { - self.get( - cache, - page.parse::().unwrap_or(1), - sort, - options, - ) - .await - } - }; + let _ = pool; + let videos: std::result::Result, Error> = self.query(cache, page.parse::().unwrap_or(1), query.unwrap_or("".to_string()).as_str(), options) + .await; match videos { Ok(v) => v, Err(e) => { @@ -387,4 +270,7 @@ impl Provider for PmvhavenProvider { } } } + fn get_channel(&self, clientversion: ClientVersion) -> crate::status::Channel { + self.build_channel(clientversion) + } } diff --git a/src/videos.rs b/src/videos.rs index 5983241..601486d 100644 --- a/src/videos.rs +++ b/src/videos.rs @@ -48,6 +48,7 @@ pub struct ServerOptions { pub stars: Option, // pub categories: Option, // pub duration: Option, // + pub sort: Option, // } #[derive(serde::Serialize, Debug)]