pmvhaven
This commit is contained in:
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
cute = "0.3.0"
|
||||||
diesel = { version = "2.2.10", features = ["sqlite", "r2d2"] }
|
diesel = { version = "2.2.10", features = ["sqlite", "r2d2"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
|
|||||||
79
src/api.rs
79
src/api.rs
@@ -1,15 +1,16 @@
|
|||||||
use std::cmp::Ordering;
|
|
||||||
use ntex::http::header;
|
use ntex::http::header;
|
||||||
use ntex::web;
|
use ntex::web;
|
||||||
use ntex::web::HttpRequest;
|
use ntex::web::HttpRequest;
|
||||||
use tokio::{task};
|
use std::cmp::Ordering;
|
||||||
|
use tokio::task;
|
||||||
|
|
||||||
use crate::providers::hanime::HanimeProvider;
|
use crate::providers::hanime::HanimeProvider;
|
||||||
use crate::providers::perverzija::PerverzijaProvider;
|
use crate::providers::perverzija::PerverzijaProvider;
|
||||||
|
use crate::providers::pmvhaven::PmvhavenProvider;
|
||||||
use crate::providers::pornhub::PornhubProvider;
|
use crate::providers::pornhub::PornhubProvider;
|
||||||
use crate::providers::spankbang::SpankbangProvider;
|
use crate::providers::spankbang::SpankbangProvider;
|
||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::{providers::*, status::*, videos::*, DbPool};
|
use crate::{DbPool, providers::*, status::*, videos::*};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ClientVersion {
|
struct ClientVersion {
|
||||||
@@ -18,14 +19,12 @@ struct ClientVersion{
|
|||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl ClientVersion {
|
impl ClientVersion {
|
||||||
|
|
||||||
pub fn new(version: u32, subversion: u32, name: String) -> ClientVersion {
|
pub fn new(version: u32, subversion: u32, name: String) -> ClientVersion {
|
||||||
ClientVersion {
|
ClientVersion {
|
||||||
version,
|
version,
|
||||||
subversion,
|
subversion,
|
||||||
name
|
name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +36,8 @@ impl ClientVersion {
|
|||||||
let name = name_version[1];
|
let name = name_version[1];
|
||||||
|
|
||||||
// Extract version and optional subversion
|
// Extract version and optional subversion
|
||||||
let (version, subversion) = if let Some((v, c)) = name.split_at(name.len().saturating_sub(1)).into() {
|
let (version, subversion) =
|
||||||
|
if let Some((v, c)) = name.split_at(name.len().saturating_sub(1)).into() {
|
||||||
match v.parse::<u32>() {
|
match v.parse::<u32>() {
|
||||||
Ok(ver) => (ver, c.chars().next().map(|ch| ch as u32).unwrap_or(0)),
|
Ok(ver) => (ver, c.chars().next().map(|ch| ch as u32).unwrap_or(0)),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -99,16 +99,15 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
||||||
|
|
||||||
let clientversion: ClientVersion = match req.headers().get("User-Agent") {
|
let clientversion: ClientVersion = match req.headers().get("User-Agent") {
|
||||||
Some(v) => match v.to_str() {
|
Some(v) => match v.to_str() {
|
||||||
Ok(useragent) => ClientVersion::parse(useragent).unwrap_or_else(|| ClientVersion::new(999, 0, "999".to_string())),
|
Ok(useragent) => ClientVersion::parse(useragent)
|
||||||
Err(_) => ClientVersion::new(999, 0, "999".to_string())
|
.unwrap_or_else(|| ClientVersion::new(999, 0, "999".to_string())),
|
||||||
|
Err(_) => ClientVersion::new(999, 0, "999".to_string()),
|
||||||
},
|
},
|
||||||
_=> ClientVersion::new(999, 0, "999".to_string())
|
_ => ClientVersion::new(999, 0, "999".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
let host = req
|
let host = req
|
||||||
.headers()
|
.headers()
|
||||||
.get(header::HOST)
|
.get(header::HOST)
|
||||||
@@ -127,6 +126,17 @@ async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
|||||||
options: vec![],
|
options: vec![],
|
||||||
nsfw: true,
|
nsfw: true,
|
||||||
});
|
});
|
||||||
|
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![],
|
||||||
|
nsfw: true,
|
||||||
|
});
|
||||||
if clientversion >= ClientVersion::new(22, 97, "22a".to_string()) {
|
if clientversion >= ClientVersion::new(22, 97, "22a".to_string()) {
|
||||||
//add perverzija
|
//add perverzija
|
||||||
status.add_channel(Channel {
|
status.add_channel(Channel {
|
||||||
@@ -134,7 +144,8 @@ async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
|||||||
name: "Perverzija".to_string(),
|
name: "Perverzija".to_string(),
|
||||||
description: "Free videos from Perverzija".to_string(),
|
description: "Free videos from Perverzija".to_string(),
|
||||||
premium: false,
|
premium: false,
|
||||||
favicon: "https://www.google.com/s2/favicons?sz=64&domain=tube.perverzija.com".to_string(),
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=tube.perverzija.com"
|
||||||
|
.to_string(),
|
||||||
status: "active".to_string(),
|
status: "active".to_string(),
|
||||||
categories: vec![],
|
categories: vec![],
|
||||||
options: vec![
|
options: vec![
|
||||||
@@ -204,8 +215,7 @@ async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
|||||||
favicon: "https://www.google.com/s2/favicons?sz=64&domain=hanime.tv".to_string(),
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=hanime.tv".to_string(),
|
||||||
status: "active".to_string(),
|
status: "active".to_string(),
|
||||||
categories: vec![],
|
categories: vec![],
|
||||||
options: vec![
|
options: vec![ChannelOption {
|
||||||
ChannelOption{
|
|
||||||
id: "sort".to_string(),
|
id: "sort".to_string(),
|
||||||
title: "Sort".to_string(),
|
title: "Sort".to_string(),
|
||||||
description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".to_string(),
|
description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".to_string(),
|
||||||
@@ -254,8 +264,7 @@ async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
}
|
}],
|
||||||
],
|
|
||||||
nsfw: true,
|
nsfw: true,
|
||||||
});
|
});
|
||||||
status.add_channel(Channel {
|
status.add_channel(Channel {
|
||||||
@@ -266,8 +275,7 @@ async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
|||||||
favicon: "https://www.google.com/s2/favicons?sz=64&domain=spankbang.com".to_string(),
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=spankbang.com".to_string(),
|
||||||
status: "active".to_string(),
|
status: "active".to_string(),
|
||||||
categories: vec![],
|
categories: vec![],
|
||||||
options: vec![
|
options: vec![ChannelOption {
|
||||||
ChannelOption{
|
|
||||||
id: "sort".to_string(),
|
id: "sort".to_string(),
|
||||||
title: "Sort".to_string(),
|
title: "Sort".to_string(),
|
||||||
description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".to_string(),
|
description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".to_string(),
|
||||||
@@ -285,10 +293,10 @@ async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
|||||||
FilterOption {
|
FilterOption {
|
||||||
id: "most_popular".to_string(),
|
id: "most_popular".to_string(),
|
||||||
title: "Popular".to_string(),
|
title: "Popular".to_string(),
|
||||||
}],
|
},
|
||||||
multiSelect: false,
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
|
multiSelect: false,
|
||||||
|
}],
|
||||||
nsfw: true,
|
nsfw: true,
|
||||||
});
|
});
|
||||||
status.iconUrl = format!("http://{}/favicon.ico", host).to_string();
|
status.iconUrl = format!("http://{}/favicon.ico", host).to_string();
|
||||||
@@ -331,11 +339,31 @@ async fn videos_post(
|
|||||||
.to_string()
|
.to_string()
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let featured = video_request.featured.as_deref().unwrap_or("all").to_string();
|
let featured = video_request
|
||||||
|
.featured
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("all")
|
||||||
|
.to_string();
|
||||||
let provider = get_provider(channel.as_str())
|
let provider = get_provider(channel.as_str())
|
||||||
.ok_or_else(|| web::error::ErrorBadRequest("Invalid channel".to_string()))?;
|
.ok_or_else(|| web::error::ErrorBadRequest("Invalid channel".to_string()))?;
|
||||||
|
let category = video_request
|
||||||
|
.category
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("all")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let video_items = provider
|
let video_items = provider
|
||||||
.get_videos(cache.get_ref().clone(), pool.get_ref().clone(), channel.clone(), sort.clone(), query.clone(), page.to_string(), perPage.to_string(), featured.clone())
|
.get_videos(
|
||||||
|
cache.get_ref().clone(),
|
||||||
|
pool.get_ref().clone(),
|
||||||
|
channel.clone(),
|
||||||
|
sort.clone(),
|
||||||
|
query.clone(),
|
||||||
|
page.to_string(),
|
||||||
|
perPage.to_string(),
|
||||||
|
featured.clone(),
|
||||||
|
category.clone()
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
videos.items = video_items.clone();
|
videos.items = video_items.clone();
|
||||||
if video_items.len() == 0 {
|
if video_items.len() == 0 {
|
||||||
@@ -354,6 +382,7 @@ async fn videos_post(
|
|||||||
let query_clone = query.clone();
|
let query_clone = query.clone();
|
||||||
let per_page_clone = perPage.to_string();
|
let per_page_clone = perPage.to_string();
|
||||||
let featured_clone = featured.clone();
|
let featured_clone = featured.clone();
|
||||||
|
let category_clone = category.clone();
|
||||||
task::spawn_local(async move {
|
task::spawn_local(async move {
|
||||||
// if let AnyProvider::Spankbang(_) = provider_clone {
|
// if let AnyProvider::Spankbang(_) = provider_clone {
|
||||||
// // Spankbang has a delay for the next page
|
// // Spankbang has a delay for the next page
|
||||||
@@ -369,6 +398,7 @@ async fn videos_post(
|
|||||||
next_page.to_string(),
|
next_page.to_string(),
|
||||||
per_page_clone,
|
per_page_clone,
|
||||||
featured_clone,
|
featured_clone,
|
||||||
|
category_clone,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
@@ -382,6 +412,7 @@ pub fn get_provider(channel: &str) -> Option<AnyProvider> {
|
|||||||
"hanime" => Some(AnyProvider::Hanime(HanimeProvider::new())),
|
"hanime" => Some(AnyProvider::Hanime(HanimeProvider::new())),
|
||||||
"spankbang" => Some(AnyProvider::Spankbang(SpankbangProvider::new())),
|
"spankbang" => Some(AnyProvider::Spankbang(SpankbangProvider::new())),
|
||||||
"pornhub" => Some(AnyProvider::Pornhub(PornhubProvider::new())),
|
"pornhub" => Some(AnyProvider::Pornhub(PornhubProvider::new())),
|
||||||
|
"pmvhaven" => Some(AnyProvider::Pmvhaven(PmvhavenProvider::new())),
|
||||||
_ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())),
|
_ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#![warn(unused_extern_crates)]
|
#![warn(unused_extern_crates)]
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
|
||||||
use diesel::{r2d2::{self, ConnectionManager}, SqliteConnection};
|
use diesel::{r2d2::{self, ConnectionManager}, SqliteConnection};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use ntex_files as fs;
|
use ntex_files as fs;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::vec;
|
use std::vec;
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
|
use serde_json::error::Category;
|
||||||
use wreq::Client;
|
use wreq::Client;
|
||||||
use wreq_util::Emulation;
|
use wreq_util::Emulation;
|
||||||
use crate::db;
|
use crate::db;
|
||||||
@@ -264,7 +265,9 @@ impl Provider for HanimeProvider {
|
|||||||
page: String,
|
page: String,
|
||||||
per_page: String,
|
per_page: String,
|
||||||
featured: String,
|
featured: String,
|
||||||
|
category: String,
|
||||||
) -> Vec<VideoItem> {
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = category;
|
||||||
let _ = featured;
|
let _ = featured;
|
||||||
let _ = per_page;
|
let _ = per_page;
|
||||||
let _ = sort;
|
let _ = sort;
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
use crate::{providers::{hanime::HanimeProvider, perverzija::PerverzijaProvider, pornhub::PornhubProvider, spankbang::SpankbangProvider}, util::cache::VideoCache, videos::VideoItem, DbPool};
|
use crate::{
|
||||||
|
DbPool,
|
||||||
|
providers::{
|
||||||
|
hanime::HanimeProvider, perverzija::PerverzijaProvider, pmvhaven::PmvhavenProvider,
|
||||||
|
pornhub::PornhubProvider, spankbang::SpankbangProvider,
|
||||||
|
},
|
||||||
|
util::cache::VideoCache,
|
||||||
|
videos::VideoItem,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod perverzija;
|
|
||||||
pub mod hanime;
|
pub mod hanime;
|
||||||
pub mod spankbang;
|
pub mod perverzija;
|
||||||
|
pub mod pmvhaven;
|
||||||
pub mod pornhub;
|
pub mod pornhub;
|
||||||
|
pub mod spankbang;
|
||||||
|
|
||||||
pub trait Provider {
|
pub trait Provider {
|
||||||
async fn get_videos(&self, cache: VideoCache, pool: DbPool, channel: String, sort: String, query: Option<String>, page: String, per_page: String, featured: String) -> Vec<VideoItem>;
|
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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -15,18 +35,60 @@ pub enum AnyProvider {
|
|||||||
Hanime(HanimeProvider),
|
Hanime(HanimeProvider),
|
||||||
Spankbang(SpankbangProvider),
|
Spankbang(SpankbangProvider),
|
||||||
Pornhub(PornhubProvider),
|
Pornhub(PornhubProvider),
|
||||||
|
Pmvhaven(PmvhavenProvider),
|
||||||
}
|
}
|
||||||
impl Provider for AnyProvider {
|
impl Provider for AnyProvider {
|
||||||
async fn get_videos(&self, cache: VideoCache, pool:DbPool, channel: String, sort: String, query: Option<String>, page: String, per_page: String, featured: String) -> Vec<VideoItem> {
|
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> {
|
||||||
println!(
|
println!(
|
||||||
"/api/videos: channel={:?}, sort={:?}, query={:?}, page={:?}, per_page={:?}, featured={:?}",
|
"/api/videos: channel={:?}, sort={:?}, query={:?}, page={:?}, per_page={:?}, featured={:?}",
|
||||||
channel, sort, query, page, per_page, featured
|
channel, sort, query, page, per_page, featured
|
||||||
);
|
);
|
||||||
match self {
|
match self {
|
||||||
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::Perverzija(p) => {
|
||||||
AnyProvider::Hanime(p) => p.get_videos(cache, pool, channel, sort, query, page, per_page, featured).await,
|
p.get_videos(
|
||||||
AnyProvider::Spankbang(p) => p.get_videos(cache, pool, channel, sort, query, page, per_page, featured).await,
|
cache.clone(),
|
||||||
AnyProvider::Pornhub(p) => p.get_videos(cache, pool, channel, sort, query, page, per_page, featured).await,
|
pool.clone(),
|
||||||
|
channel.clone(),
|
||||||
|
sort.clone(),
|
||||||
|
query.clone(),
|
||||||
|
page.clone(),
|
||||||
|
per_page.clone(),
|
||||||
|
featured.clone(),
|
||||||
|
category.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AnyProvider::Hanime(p) => {
|
||||||
|
p.get_videos(cache, pool, channel, sort, query, page, per_page, featured,
|
||||||
|
category.clone(),)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AnyProvider::Spankbang(p) => {
|
||||||
|
p.get_videos(cache, pool, channel, sort, query, page, per_page, featured,
|
||||||
|
category.clone(),)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AnyProvider::Pornhub(p) => {
|
||||||
|
p.get_videos(cache, pool, channel, sort, query, page, per_page, featured,
|
||||||
|
category.clone(),)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
AnyProvider::Pmvhaven(p) => {
|
||||||
|
p.get_videos(cache, pool, channel, sort, query, page, per_page, featured,
|
||||||
|
category.clone(),)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -452,7 +452,9 @@ impl Provider for PerverzijaProvider {
|
|||||||
page: String,
|
page: String,
|
||||||
per_page: String,
|
per_page: String,
|
||||||
featured: String,
|
featured: String,
|
||||||
|
category: String,
|
||||||
) -> Vec<VideoItem> {
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = category;
|
||||||
let _ = per_page;
|
let _ = per_page;
|
||||||
let _ = sort;
|
let _ = sort;
|
||||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||||
|
|||||||
422
src/providers/pmvhaven.rs
Normal file
422
src/providers/pmvhaven.rs
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
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<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 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<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 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.101:8080").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 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.101:8080").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![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ impl PornhubProvider {
|
|||||||
|
|
||||||
let old_items = match cache.get(&url) {
|
let old_items = match cache.get(&url) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
println!("Cache hit for URL: {}", url);
|
println!("Cache hit for URL: {}", url);
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
} else {
|
} else {
|
||||||
@@ -110,7 +110,7 @@ impl PornhubProvider {
|
|||||||
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||||
let old_items = match cache.get(&url) {
|
let old_items = match cache.get(&url) {
|
||||||
Some((time, items)) => {
|
Some((time, items)) => {
|
||||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||||
return Ok(items.clone());
|
return Ok(items.clone());
|
||||||
} else {
|
} else {
|
||||||
let _ = cache.check().await;
|
let _ = cache.check().await;
|
||||||
@@ -251,7 +251,9 @@ impl Provider for PornhubProvider {
|
|||||||
page: String,
|
page: String,
|
||||||
per_page: String,
|
per_page: String,
|
||||||
featured: String,
|
featured: String,
|
||||||
|
category: String,
|
||||||
) -> Vec<VideoItem> {
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = category;
|
||||||
let _ = per_page;
|
let _ = per_page;
|
||||||
let _ = sort;
|
let _ = sort;
|
||||||
let _ = featured; // Ignored in this implementation
|
let _ = featured; // Ignored in this implementation
|
||||||
|
|||||||
@@ -358,7 +358,9 @@ impl Provider for SpankbangProvider {
|
|||||||
page: String,
|
page: String,
|
||||||
per_page: String,
|
per_page: String,
|
||||||
featured: String,
|
featured: String,
|
||||||
|
category: String,
|
||||||
) -> Vec<VideoItem> {
|
) -> Vec<VideoItem> {
|
||||||
|
let _ = category;
|
||||||
let _ = per_page;
|
let _ = per_page;
|
||||||
let _ = featured;
|
let _ = featured;
|
||||||
let _ = pool;
|
let _ = pool;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub struct VideosRequest {
|
|||||||
// Your server's global options will be sent in the videos request
|
// Your server's global options will be sent in the videos request
|
||||||
// pub flavor: "mint chocolate chip"
|
// pub flavor: "mint chocolate chip"
|
||||||
pub featured: Option<String>, // "featured",
|
pub featured: Option<String>, // "featured",
|
||||||
|
pub category: Option<String>, // "pmv"
|
||||||
}
|
}
|
||||||
#[derive(serde::Serialize, Debug)]
|
#[derive(serde::Serialize, Debug)]
|
||||||
pub struct PageInfo {
|
pub struct PageInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user