454 lines
16 KiB
Rust
454 lines
16 KiB
Rust
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::{VideoFormat, VideoItem};
|
|
use cute::c;
|
|
use error_chain::error_chain;
|
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
|
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
|
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<bool>, //null,
|
|
sameSexContent: Option<bool>, //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,
|
|
hideUntagged: bool, //true,
|
|
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,
|
|
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<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),
|
|
// format!("https://storage.pmvhaven.com/{}/{}_{}.mp4", self._id.clone(), self.title.replace(" ","-"), self._id),
|
|
"pmvhaven".to_string(),
|
|
thumbnail,
|
|
self.duration as u32,
|
|
)
|
|
//.formats(vec![
|
|
// VideoFormat::new(format!("https://storage.pmvhaven.com/{}/{}_{}.mp4", video_id, encoded_title, video_id), "1080".to_string(), "mp4".to_string()).protocol("https".to_string()),
|
|
// VideoFormat::new(format!("https://storage.pmvhaven.com/{}/264_{}.mp4", video_id, video_id), "1080".to_string(), "mp4".to_string()).protocol("https".to_string())
|
|
//])
|
|
|
|
.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;
|
|
}
|
|
}
|
|
|
|
// Define a percent-encoding set that encodes all non-ASCII characters
|
|
const EMOJI_ENCODE_SET: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'%').add(b'<').add(b'>').add(b'?').add(b'[').add(b'\\').add(b']').add(b'^').add(b'`').add(b'{').add(b'|').add(b'}');
|
|
|
|
// Helper function to percent-encode emojis and other non-ASCII chars
|
|
fn percent_encode_emojis(s: &str) -> String {
|
|
utf8_percent_encode(s, EMOJI_ENCODE_SET).to_string()
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct PmvhavenResponse {
|
|
httpStatusCode: Option<u32>,
|
|
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,
|
|
}
|
|
impl PmvhavenProvider {
|
|
pub fn new() -> Self {
|
|
PmvhavenProvider {
|
|
url: "https://pmvhaven.com".to_string(),
|
|
}
|
|
}
|
|
async fn get(&self, cache: VideoCache, page: u8, category: String) -> Result<Vec<VideoItem>> {
|
|
let index = format!("pmvhaven:{}:{}", page, category);
|
|
let url = format!("{}/api/getmorevideos", self.url);
|
|
let mut request = PmvhavenRequest::new(page as u32);
|
|
println!("Category: {}", category);
|
|
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()
|
|
}
|
|
}
|
|
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::<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();
|
|
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<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) {
|
|
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::<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();
|
|
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<String>,
|
|
page: String,
|
|
per_page: String,
|
|
featured: String,
|
|
category: String,
|
|
) -> Vec<VideoItem> {
|
|
let _ = per_page;
|
|
let _ = sort;
|
|
let _ = featured; // Ignored in this implementation
|
|
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).await,
|
|
None => {
|
|
self.get(cache, page.parse::<u8>().unwrap_or(1), category)
|
|
.await
|
|
}
|
|
};
|
|
match videos {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
println!("Error fetching videos: {}", e);
|
|
vec![]
|
|
}
|
|
}
|
|
}
|
|
}
|