Files
hottub/src/api.rs
2025-07-11 15:40:07 +00:00

396 lines
14 KiB
Rust

use std::cmp::Ordering;
use htmlentity::types::Byte;
use ntex::http::header;
use ntex::web;
use ntex::web::HttpRequest;
use tokio::{task, time};
use crate::providers::erothots::ErothotsProvider;
use crate::providers::hanime::HanimeProvider;
use crate::providers::perverzija::PerverzijaProvider;
use crate::providers::spankbang::SpankbangProvider;
use crate::util::cache::VideoCache;
use crate::{providers::*, status::*, videos::*, DbPool};
#[derive(Debug)]
struct ClientVersion{
version: u32,
subversion: u32,
name: String,
}
impl ClientVersion {
pub fn new(version: u32, subversion: u32, name: String) -> ClientVersion{
ClientVersion{
version,
subversion,
name
}
}
pub fn parse(input: &str) -> Option<Self> {
// Example input: "Hot%20Tub/22c CFNetwork/1494.0.7 Darwin/23.4.0 0.002478"
let parts: Vec<&str> = input.split_whitespace().collect();
if let Some(first) = parts.first() {
let name_version: Vec<&str> = first.split('/').collect();
let name = name_version[1];
// Extract version and optional subversion
let (version, subversion) = if let Some((v, c)) = name.split_at(name.len().saturating_sub(1)).into() {
match v.parse::<u32>() {
Ok(ver) => (ver, c.chars().next().map(|ch| ch as u32).unwrap_or(0)),
Err(_) => {
// Try parsing whole string if no subversion exists
match name.parse::<u32>() {
Ok(ver) => (ver, 0),
Err(_) => return None,
}
}
}
} else {
return None;
};
return Some(ClientVersion {
version: version,
subversion: subversion,
name: name.to_string(),
});
}
None
}
}
// Implement comparisons
impl PartialEq for ClientVersion {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for ClientVersion {}
impl PartialOrd for ClientVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ClientVersion {
fn cmp(&self, other: &Self) -> Ordering {
self.version
.cmp(&other.version)
.then_with(|| self.subversion.cmp(&other.subversion))
}
}
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::resource("/status")
.route(web::post().to(status))
.route(web::get().to(status)),
)
.service(
web::resource("/videos")
// .route(web::get().to(videos_get))
.route(web::post().to(videos_post)),
);
}
async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
let clientversion: ClientVersion = match req.headers().get("User-Agent"){
Some(v) => match v.to_str(){
Ok(useragent) => ClientVersion::parse(useragent).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())
};
let host = req
.headers()
.get(header::HOST)
.and_then(|h| h.to_str().ok())
.unwrap_or_default()
.to_string();
let mut status = Status::new();
if clientversion >= ClientVersion::new(22,97,"22a".to_string()){
//add perverzija
status.add_channel(Channel {
id: "perverzija".to_string(),
name: "Perverzija".to_string(),
description: "Free videos from Perverzija".to_string(),
premium: false,
favicon: "https://www.google.com/s2/favicons?sz=64&domain=tube.perverzija.com".to_string(),
status: "active".to_string(),
categories: vec![],
options: vec![
// ChannelOption {
// id: "sort".to_string(),
// title: "Sort".to_string(),
// description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".to_string(),
// systemImage: "list.number".to_string(),
// colorName: "blue".to_string(),
// options: vec![
// FilterOption {
// id: "date".to_string(),
// title: "Date".to_string(),
// },
// FilterOption {
// id: "name".to_string(),
// title: "Name".to_string(),
// },
// ],
// multiSelect: false,
// },
ChannelOption {
id: "featured".to_string(),
title: "Featured".to_string(),
description: "Filter Featured Videos.".to_string(),
systemImage: "star".to_string(),
colorName: "red".to_string(),
options: vec![
FilterOption {
id: "all".to_string(),
title: "No".to_string(),
},
FilterOption {
id: "featured".to_string(),
title: "Yes".to_string(),
},
],
multiSelect: false,
},
// ChannelOption {
// id: "duration".to_string(),
// title: "Duration".to_string(),
// description: "Filter the videos by duration.".to_string(),
// systemImage: "timer".to_string(),
// colorName: "green".to_string(),
// options: vec![
// FilterOption {
// id: "short".to_string(),
// title: "< 1h".to_string(),
// },
// FilterOption {
// id: "long".to_string(),
// title: "> 1h".to_string(),
// },
// ],
// multiSelect: true,
// },
],
nsfw: true,
});
}
status.add_channel(Channel {
id: "hanime".to_string(),
name: "Hanime".to_string(),
description: "Free Hentai from Hanime".to_string(),
premium: false,
favicon: "https://www.google.com/s2/favicons?sz=64&domain=hanime.tv".to_string(),
status: "active".to_string(),
categories: vec![],
options: vec![
ChannelOption{
id: "sort".to_string(),
title: "Sort".to_string(),
description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".to_string(),
systemImage: "list.number".to_string(),
colorName: "blue".to_string(),
options: vec![
FilterOption {
id: "created_at_unix.desc".to_string(),
title: "Recent Upload".to_string(),
},
FilterOption {
id: "created_at_unix.asc".to_string(),
title: "Old Upload".to_string(),
},
FilterOption {
id: "views.desc".to_string(),
title: "Most Views".to_string(),
},
FilterOption {
id: "views.asc".to_string(),
title: "Least Views".to_string(),
},
FilterOption {
id: "likes.desc".to_string(),
title: "Most Likes".to_string(),
},
FilterOption {
id: "likes.asc".to_string(),
title: "Least Likes".to_string(),
},
FilterOption {
id: "released_at_unix.desc".to_string(),
title: "New".to_string(),
},
FilterOption {
id: "released_at_unix.asc".to_string(),
title: "Old".to_string(),
},
FilterOption {
id: "title_sortable.asc".to_string(),
title: "A - Z".to_string(),
},
FilterOption {
id: "title_sortable.desc".to_string(),
title: "Z - A".to_string(),
},
],
multiSelect: false,
}
],
nsfw: true,
});
// status.add_channel(Channel {
// id: "erothots".to_string(),
// name: "Erothots".to_string(),
// description: "Free Onlyfans Content".to_string(),
// premium: false,
// favicon: "https://www.google.com/s2/favicons?sz=64&domain=erothots.co".to_string(),
// status: "active".to_string(),
// categories: vec![],
// options: vec![],
// nsfw: true,
// });
// status.add_channel(Channel {
// id: "fapello".to_string(),
// name: "Fapello".to_string(),
// description: "Free Onlyfans Content".to_string(),
// premium: false,
// favicon: "https://www.google.com/s2/favicons?sz=64&domain=fapello.com".to_string(),
// status: "active".to_string(),
// categories: vec![],
// options: vec![],
// nsfw: true,
// });
status.add_channel(Channel {
id: "spankbang".to_string(),
name: "Work in Progress - SpankBang".to_string(),
description: "Popular Porn Videos - SpankBang".to_string(),
premium: false,
favicon: "https://www.google.com/s2/favicons?sz=64&domain=spankbang.com".to_string(),
status: "active".to_string(),
categories: vec![],
options: vec![
ChannelOption{
id: "sort".to_string(),
title: "Sort".to_string(),
description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".to_string(),
systemImage: "list.number".to_string(),
colorName: "blue".to_string(),
options: vec![
FilterOption {
id: "trending_videos".to_string(),
title: "Trending".to_string(),
},
FilterOption {
id: "new_videos".to_string(),
title: "New".to_string(),
},
FilterOption {
id: "most_popular".to_string(),
title: "Popular".to_string(),
}],
multiSelect: false,
}
],
nsfw: true,
});
status.iconUrl = format!("http://{}/favicon.ico", host).to_string();
Ok(web::HttpResponse::Ok().json(&status))
}
async fn videos_post(
video_request: web::types::Json<VideosRequest>,
cache: web::types::State<VideoCache>,
pool: web::types::State<DbPool>,
) -> Result<impl web::Responder, web::Error> {
let mut videos = Videos {
pageInfo: PageInfo {
hasNextPage: true,
resultsPerPage: 10,
},
items: vec![],
};
let channel: String = video_request
.channel
.as_deref()
.unwrap_or("all")
.to_string();
let sort: String = video_request.sort.as_deref().unwrap_or("date").to_string();
let mut query: Option<String> = video_request.query.clone();
if video_request.query.as_deref() == Some("") {
query = None;
}
let page: u8 = video_request
.page
.as_deref()
.unwrap_or("1")
.to_string()
.parse()
.unwrap();
let perPage: u8 = video_request
.perPage
.as_deref()
.unwrap_or("10")
.to_string()
.parse()
.unwrap();
let featured = video_request.featured.as_deref().unwrap_or("all").to_string();
let provider = get_provider(channel.as_str())
.ok_or_else(|| web::error::ErrorBadRequest("Invalid channel".to_string()))?;
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())
.await;
videos.items = video_items.clone();
if video_items.len() == 0 {
videos.pageInfo = PageInfo {
hasNextPage: false,
resultsPerPage: 10,
}
}
//###
let next_page = page.to_string().parse::<i32>().unwrap_or(1) + 1;
let provider_clone = provider.clone();
let cache_clone = cache.get_ref().clone();
let pool_clone = pool.get_ref().clone();
let channel_clone = channel.clone();
let sort_clone = sort.clone();
let query_clone = query.clone();
let per_page_clone = perPage.to_string();
let featured_clone = featured.clone();
task::spawn_local(async move {
let _ = provider_clone
.get_videos(
cache_clone,
pool_clone,
channel_clone,
sort_clone,
query_clone,
next_page.to_string(),
per_page_clone,
featured_clone,
)
.await;
});
//###
Ok(web::HttpResponse::Ok().json(&videos))
}
pub fn get_provider(channel: &str) -> Option<AnyProvider> {
match channel {
"perverzija" => Some(AnyProvider::Perverzija(PerverzijaProvider::new())),
"hanime" => Some(AnyProvider::Hanime(HanimeProvider::new())),
"spankbang" => Some(AnyProvider::Spankbang(SpankbangProvider::new())),
"erothots" => Some(AnyProvider::Erothots(ErothotsProvider::new())), // ErothotsProvider is not implemented yet
_ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())),
}
}