pmvhaven backend fix

This commit is contained in:
Simon
2025-11-29 17:16:21 +00:00
parent 8f885c79d4
commit 5522f2e37d
4 changed files with 202 additions and 391 deletions

View File

@@ -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<impl web::Responder, web::Error> {
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<DynProvider> {
"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())),

View File

@@ -50,6 +50,7 @@ pub static ALL_PROVIDERS: Lazy<HashMap<&'static str, DynProvider>> = 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
});

View File

@@ -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<bool>, //null,
sameSexContent: Option<bool>, //null,
transContent: Option<String>, //null
seizureWarning: Option<bool>, //null,
tags: Vec<String>, //[],
music: Vec<String>, //[],
stars: Vec<String>, //[],
creators: Vec<String>, //[],
range: Vec<u32>, //[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<String>, //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<String>, //itonlygetsworse",
duration: f32, //259.093333,
_width: Option<String>, //3840",
_height: Option<String>, //2160",
_ratio: Option<u32>, //50,
thumbnails: Vec<Option<String>>, //[
// "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<String>, //https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/JAV Addiction Therapy_686f24e96f7124f3dfbe90ab.mp4",
previewUrlCompressed: Option<String>, //https://storage.pmvhaven.com/686f24e96f7124f3dfbe90ab/videoPreview/comus_686f24e96f7124f3dfbe90ab.mp4",
_seizureWarning: Option<bool>, //false,
_isoDate: Option<String>, //2025-07-10T02:52:26.000Z",
_gayContent: Option<bool>, //false,
_transContent: Option<bool>, //false,
creator: Option<String>, //itonlygetsworse",
_id: String, //686f2aeade2062f93d72931f",
_totalRaters: Option<u32>, //42,
_rating: Option<u32>, //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::<Vec<&str>>().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<PmvhavenVideo>,
_count: Option<u32>,
}
impl PmvhavenResponse {
fn to_videoitems(self) -> Vec<VideoItem> {
return c![video.to_videoitem(), for video in self.data];
}
}
#[derive(Debug, Clone)]
pub struct PmvhavenProvider {
url: String,
stars: Arc<RwLock<Vec<String>>>,
categories: Arc<RwLock<Vec<String>>>,
}
impl PmvhavenProvider {
pub fn new() -> Self {
PmvhavenProvider {
let provider = PmvhavenProvider {
url: "https://pmvhaven.com".to_string(),
}
}
async fn get(
&self,
cache: VideoCache,
page: u8,
sort: String,
options: ServerOptions,
) -> Result<Vec<VideoItem>> {
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,
stars: Arc::new(RwLock::new(vec![])),
categories: Arc::new(RwLock::new(vec![])),
};
provider
}
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()
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,
}
}
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::<PmvhavenResponse>().await {
Ok(resp) => resp,
Err(e) => {
println!("Failed to parse PmvhavenResponse: {}", e);
return Ok(old_items);
// Push one item with minimal lock time and dedup by id
fn push_unique(target: &Arc<RwLock<Vec<String>>>, 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);
}
};
let video_items: Vec<VideoItem> = 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<Vec<VideoItem>> {
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,22 +175,74 @@ 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::<PmvhavenResponse>().await {
Ok(resp) => resp,
Err(e) => {
println!("Failed to parse PmvhavenResponse: {}", e);
return Ok(old_items);
}
};
let video_items: Vec<VideoItem> = videos.to_videoitems();
let text = requester.get(&video_url).await.unwrap();
let json = serde_json::from_str::<serde_json::Value>(&text).unwrap_or(serde_json::Value::Null);
let video_items: Vec<VideoItem> = 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<VideoItem> {
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::<Vec<_>>();
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;
}
}
@@ -356,29 +252,16 @@ impl Provider for PmvhavenProvider {
&self,
cache: VideoCache,
pool: DbPool,
sort: String,
_sort: String,
query: Option<String>,
page: String,
per_page: String,
options: ServerOptions,
) -> Vec<VideoItem> {
let _ = per_page;
let _ = pool; // Ignored in this implementation
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
Some(q) => {
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, options)
.await
}
None => {
self.get(
cache,
page.parse::<u8>().unwrap_or(1),
sort,
options,
)
.await
}
};
let _ = pool;
let videos: std::result::Result<Vec<VideoItem>, Error> = self.query(cache, page.parse::<u8>().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)
}
}

View File

@@ -48,6 +48,7 @@ pub struct ServerOptions {
pub stars: Option<String>, //
pub categories: Option<String>, //
pub duration: Option<String>, //
pub sort: Option<String>, //
}
#[derive(serde::Serialize, Debug)]