init
This commit is contained in:
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "hottub"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.88"
|
||||
awc = "3.7.0"
|
||||
env_logger = "0.11.8"
|
||||
error-chain = "0.12.4"
|
||||
futures = "0.3.31"
|
||||
html5ever = "0.31.0"
|
||||
htmlentity = "1.3.2"
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
ntex = { version = "2.0", features = ["tokio", "openssl"] }
|
||||
ntex-files = "2.0.0"
|
||||
once_cell = "1.21.3"
|
||||
openssl = "0.10.73"
|
||||
reqwest = { version = "0.12.18", features = ["blocking", "json", "rustls-tls"] }
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
skyscraper = "0.6.4"
|
||||
sxd-document = "0.3.2"
|
||||
sxd-xpath = "0.4.2"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# hottub
|
||||
|
||||
Rust based hottub server
|
||||
|
||||
the following URL:
|
||||
|
||||
[hottub.spacemoehre.de](hottub://source?url=hottub.spacemoehre.de)
|
||||
274
src/api.rs
Normal file
274
src/api.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use htmlentity::entity::decode;
|
||||
use htmlentity::entity::ICodedDataTrait;
|
||||
use ntex::http::header;
|
||||
use ntex::util::Buf;
|
||||
use ntex::web;
|
||||
use ntex::web::HttpRequest;
|
||||
use ntex::web::HttpResponse;
|
||||
use serde_json::json;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::providers::perverzija::PerverzijaProvider;
|
||||
use crate::{providers::*, status::*, videos::*};
|
||||
|
||||
// this function could be located in a different module
|
||||
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 host = req
|
||||
.headers()
|
||||
.get(header::HOST)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let mut status = Status::new();
|
||||
|
||||
// You can now use `method`, `host`, and `port` as needed
|
||||
|
||||
status.add_channel(Channel {
|
||||
id: "all".to_string(),
|
||||
name: "SpaceMoehre's Hottub".to_string(),
|
||||
favicon: format!("http://{}/static/favicon.ico", host).to_string(),
|
||||
premium: false,
|
||||
description: "Work in Progress".to_string(),
|
||||
status: "active".to_string(),
|
||||
categories: vec![],
|
||||
options: vec![
|
||||
Channel_Option {
|
||||
id: "channels".to_string(),
|
||||
title: "Sites".to_string(),
|
||||
description: "Websites included in search results.".to_string(),
|
||||
systemImage: "network".to_string(),
|
||||
colorName: "purple".to_string(),
|
||||
options: vec![
|
||||
Filter_Option {
|
||||
id: "perverzija".to_string(),
|
||||
title: "Perverzija".to_string(),
|
||||
},
|
||||
],
|
||||
multiSelect: true,
|
||||
},
|
||||
Channel_Option {
|
||||
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![
|
||||
Filter_Option {
|
||||
id: "date".to_string(),
|
||||
title: "Date".to_string(),
|
||||
},
|
||||
Filter_Option {
|
||||
id: "name".to_string(),
|
||||
title: "Name".to_string(),
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
Channel_Option {
|
||||
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![
|
||||
Filter_Option {
|
||||
id: "short".to_string(),
|
||||
title: "< 1h".to_string(),
|
||||
},
|
||||
Filter_Option {
|
||||
id: "long".to_string(),
|
||||
title: "> 1h".to_string(),
|
||||
},
|
||||
],
|
||||
multiSelect: true,
|
||||
},
|
||||
Channel_Option {
|
||||
id: "featured".to_string(),
|
||||
title: "Featured".to_string(),
|
||||
description: "Filter Featured Videos.".to_string(),
|
||||
systemImage: "star".to_string(),
|
||||
colorName: "red".to_string(),
|
||||
options: vec![
|
||||
Filter_Option {
|
||||
id: "all".to_string(),
|
||||
title: "No".to_string(),
|
||||
},
|
||||
Filter_Option {
|
||||
id: "featured".to_string(),
|
||||
title: "Yes".to_string(),
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
],
|
||||
nsfw: true,
|
||||
});
|
||||
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![
|
||||
Channel_Option {
|
||||
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![
|
||||
Filter_Option {
|
||||
id: "date".to_string(),
|
||||
title: "Date".to_string(),
|
||||
},
|
||||
Filter_Option {
|
||||
id: "name".to_string(),
|
||||
title: "Name".to_string(),
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
},
|
||||
Channel_Option {
|
||||
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![
|
||||
Filter_Option {
|
||||
id: "short".to_string(),
|
||||
title: "< 1h".to_string(),
|
||||
},
|
||||
Filter_Option {
|
||||
id: "long".to_string(),
|
||||
title: "> 1h".to_string(),
|
||||
},
|
||||
],
|
||||
multiSelect: true,
|
||||
},
|
||||
],
|
||||
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<Videos_Request>,
|
||||
) -> Result<impl web::Responder, web::Error> {
|
||||
let mut format = Video_Format::new(
|
||||
"https://pervl2.xtremestream.xyz/player/xs1.php?data=794a51bb65913debd98f73111705738a"
|
||||
.to_string(),
|
||||
"1080p".to_string(),
|
||||
"m3u8".to_string(),
|
||||
);
|
||||
format.add_http_header(
|
||||
"Referer".to_string(),
|
||||
"https://pervl2.xtremestream.xyz/player/index.php?data=794a51bb65913debd98f73111705738a"
|
||||
.to_string(),
|
||||
);
|
||||
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 = PerverzijaProvider::new();
|
||||
let video_items = provider
|
||||
.get_videos(channel, sort, query, page.to_string(), perPage.to_string(), featured)
|
||||
.await;
|
||||
videos.items = video_items.clone();
|
||||
Ok(web::HttpResponse::Ok().json(&videos))
|
||||
}
|
||||
|
||||
// async fn videos_get(_req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
||||
// let mut http_headers: HashMap<String, String> = HashMap::new();
|
||||
// // http_headers.insert(
|
||||
// // "Referer".to_string(),
|
||||
// // "https://pervl2.xtremestream.xyz/player/index.php?data=794a51bb65913debd98f73111705738a"
|
||||
// // .to_string(),
|
||||
// // );
|
||||
// let mut format = Video_Format::new(
|
||||
// "https://pervl2.xtremestream.xyz/player/xs1.php?data=794a51bb65913debd98f73111705738a"
|
||||
// .to_string(),
|
||||
// "1080p".to_string(),
|
||||
// "m3u8".to_string(),
|
||||
// );
|
||||
// format.add_http_header(
|
||||
// "Referer".to_string(),
|
||||
// "https://pervl2.xtremestream.xyz/player/index.php?data=794a51bb65913debd98f73111705738a"
|
||||
// .to_string(),
|
||||
// );
|
||||
// let videos = Videos {
|
||||
// pageInfo: PageInfo {
|
||||
// hasNextPage: true,
|
||||
// resultsPerPage: 10,
|
||||
// },
|
||||
// items: vec![
|
||||
// Video_Item{
|
||||
// duration: 110, // 110,
|
||||
// views: Some(14622653), // 14622653,
|
||||
// rating: Some(0.0), // 0.0,
|
||||
// id: "794a51bb65913debd98f73111705738a".to_string(), // "c85017ca87477168d648727753c4ded8a35f173e22ef93743e707b296becb299",
|
||||
// title: "BrazzersExxtra – Give Me A D! The Best Of Cheerleaders".to_string(), // "20 Minutes of Adorable Kittens BEST Compilation",
|
||||
// // url: "https://tube.perverzija.com/brazzersexxtra-give-me-a-d-the-best-of-cheerleaders/".to_string(),
|
||||
// // url : "https://pervl2.xtremestream.xyz/player/xs1.php?data=794a51bb65913debd98f73111705738a".to_string(), // "https://www.youtube.com/watch?v=y0sF5xhGreA",
|
||||
// url : "https://pervl2.xtremestream.xyz/player/index.php?data=794a51bb65913debd98f73111705738a".to_string(),
|
||||
// channel: "perverzija".to_string(), // "youtube",
|
||||
// thumb: "https://tube.perverzija.com/wp-content/uploads/2025/05/BrazzersExxtra-Give-Me-A-D-The-Best-Of-Cheerleaders.jpg".to_string(), // "https://i.ytimg.com/vi/y0sF5xhGreA/hqdefault.jpg",
|
||||
// uploader: Some("Brazzers".to_string()), // "The Pet Collective",
|
||||
// uploaderUrl: Some("https://brazzers.com".to_string()), // "https://www.youtube.com/@petcollective",
|
||||
// verified: Some(false), // false,
|
||||
// tags: Some(vec![]), // [],
|
||||
// uploadedAt: Some(1741142954), // 1741142954
|
||||
// formats: Some(vec![format]), // Additional HTTP headers if needed
|
||||
|
||||
// }
|
||||
// ],
|
||||
// };
|
||||
|
||||
// println!("Video: {:?}", videos);
|
||||
// Ok(web::HttpResponse::Ok().json(&videos))
|
||||
// }
|
||||
80
src/main.rs
Normal file
80
src/main.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use ntex_files as fs;
|
||||
|
||||
use ntex::web;
|
||||
use ntex::web::HttpResponse;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
mod api;
|
||||
mod status;
|
||||
mod videos;
|
||||
mod providers;
|
||||
mod util;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct Metadata {
|
||||
name: String,
|
||||
version: String,
|
||||
author: String,
|
||||
}
|
||||
|
||||
// type getVideosFn = fn get_videos(channel: Option<String>, sort: Option<String>, query: Option<String>, page: Option<String>, per_page: Option<String>) -> Videos;
|
||||
|
||||
|
||||
async fn metadata(data: web::types::State<Metadata>) -> HttpResponse {
|
||||
async fn counter(x: String) -> String{
|
||||
for i in 1..=5 {
|
||||
println!("{}: {}", x, i);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
let meta = data.get_ref().clone();
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 1..=3 {
|
||||
let name = format!("{}-{}", meta.name, i);
|
||||
handles.push(thread::spawn(move || {
|
||||
futures::executor::block_on(counter(name.clone()))
|
||||
}));
|
||||
}
|
||||
|
||||
let results: Vec<String> = handles
|
||||
.into_iter()
|
||||
.map(|handle| handle.join().unwrap())
|
||||
.collect();
|
||||
|
||||
println!("Results: {:?}", results);
|
||||
|
||||
HttpResponse::Ok().json(
|
||||
&json!({
|
||||
"description": "A simple web server for the Hot Tub app.",
|
||||
"documentation": ""
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[ntex::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "ntex=warn");
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
env_logger::init(); // You need this to actually see logs
|
||||
|
||||
|
||||
web::HttpServer::new(|| {
|
||||
const METADATA_JSON: &str = include_str!("../metadata.json");
|
||||
let mut meta: Metadata = serde_json::from_str(METADATA_JSON).unwrap();
|
||||
web::App::new()
|
||||
.wrap(web::middleware::Logger::default())
|
||||
.state(meta.clone())
|
||||
.route("/meta", web::get().to(metadata))
|
||||
.service(web::scope("/api").configure(api::config))
|
||||
.service(fs::Files::new("/", "static"))
|
||||
})
|
||||
// .bind_openssl(("0.0.0.0", 18080), builder)?
|
||||
.bind(("0.0.0.0", 18080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
6
src/providers/mod.rs
Normal file
6
src/providers/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use crate::videos::{Video_Item, Videos};
|
||||
|
||||
pub mod perverzija;
|
||||
pub trait Provider{
|
||||
async fn get_videos(&self, channel: String, sort: String, query: Option<String>, page: String, per_page: String, featured: String) -> Vec<Video_Item>;
|
||||
}
|
||||
175
src/providers/perverzija.rs
Normal file
175
src/providers/perverzija.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use std::vec;
|
||||
|
||||
use error_chain::error_chain;
|
||||
use htmlentity::entity::{decode, encode, CharacterSet, EncodeType, ICodedDataTrait};
|
||||
use htmlentity::types::{AnyhowResult, Byte};
|
||||
use reqwest::Proxy;
|
||||
|
||||
use crate::providers::Provider;
|
||||
use crate::util::time::parse_time_to_seconds;
|
||||
use crate::videos::{self, PageInfo, Video_Embed, Video_Item, Videos}; // Make sure Provider trait is imported
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
HttpRequest(reqwest::Error);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PerverzijaProvider {
|
||||
url: String,
|
||||
}
|
||||
impl PerverzijaProvider {
|
||||
pub fn new() -> Self {
|
||||
PerverzijaProvider {
|
||||
url: "https://tube.perverzija.com/".to_string(),
|
||||
}
|
||||
}
|
||||
async fn get(&self, page: &u8, featured: String) -> Result<Vec<Video_Item>> {
|
||||
let mut prefix_uri = "".to_string();
|
||||
if featured == "featured"{
|
||||
prefix_uri = "featured-scenes/".to_string();
|
||||
}
|
||||
let mut url = format!("{}{}page/{}/", self.url, prefix_uri, page);
|
||||
if page == &1 {
|
||||
url = format!("{}{}", self.url, prefix_uri);
|
||||
}
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15")
|
||||
// .proxy(Proxy::https("http://192.168.0.101:8080").unwrap())
|
||||
// .danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
let response = client.get(url).send().await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items = self.get_video_items_from_html(text.clone());
|
||||
Ok(video_items)
|
||||
} else {
|
||||
Err("Failed to fetch data".into())
|
||||
}
|
||||
}
|
||||
fn query(&self, query: &str) -> Result<Vec<Video_Item>> {
|
||||
println!("Searching for query: {}", query);
|
||||
let url = format!("{}?s={}", self.url, query);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.get(&url).send()?;
|
||||
if response.status().is_success() {
|
||||
let text = response.text().unwrap_or_default();
|
||||
|
||||
println!("{}", &text);
|
||||
Ok(vec![])
|
||||
} else {
|
||||
Err("Failed to fetch data".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_video_items_from_html(&self, html: String) -> Vec<Video_Item> {
|
||||
let mut items: Vec<Video_Item> = Vec::new();
|
||||
|
||||
let raw_html = html.split("video-listing-content").collect::<Vec<&str>>();
|
||||
|
||||
let video_listing_content = raw_html[1];
|
||||
let raw_videos = video_listing_content
|
||||
.split("video-item post")
|
||||
.collect::<Vec<&str>>()[1..]
|
||||
.to_vec();
|
||||
|
||||
for video_segment in &raw_videos {
|
||||
|
||||
let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||
let mut index = 0;
|
||||
if vid.len() > 10 {
|
||||
|
||||
continue;
|
||||
}
|
||||
for line in vid.clone(){
|
||||
println!("{}: {}\n\n", index, line);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let mut title = vid[1].split(">").collect::<Vec<&str>>()[1]
|
||||
.split("<")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
// html decode
|
||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||
let url = vid[1].split("iframe src="").collect::<Vec<&str>>()[1]
|
||||
.split(""")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string().replace("index.php", "xs1.php");;
|
||||
let id = url.split("data=").collect::<Vec<&str>>()[1]
|
||||
.split("&")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let raw_duration = match vid.len(){
|
||||
10 => vid[6].split("time_dur\">").collect::<Vec<&str>>()[1]
|
||||
.split("<")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string(),
|
||||
_ => "00:00".to_string(),
|
||||
};
|
||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||
|
||||
let thumb = match vid[4].contains("srcset=") {
|
||||
true => vid[4].split("sizes=").collect::<Vec<&str>>()[1]
|
||||
.split("w, ")
|
||||
.collect::<Vec<&str>>()
|
||||
.last()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.split(" ")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string(),
|
||||
false => vid[4].split("src=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string(),
|
||||
};
|
||||
let mut embed_html = vid[1].split("data-embed='").collect::<Vec<&str>>()[1].split("'").collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
embed_html = embed_html.replace("index.php", "xs1.php");
|
||||
|
||||
println!("Embed HTML: {}\n\n", embed_html);
|
||||
println!("Url: {}\n\n", url.clone());
|
||||
let embed = Video_Embed::new(embed_html, url.clone());
|
||||
let mut video_item =
|
||||
Video_Item::new(id, title, url.clone(), "perverzija".to_string(), thumb, duration);
|
||||
video_item.embed = Some(embed);
|
||||
let mut format = videos::Video_Format::new(url.clone(), "1080".to_string(), "m3u8".to_string());
|
||||
format.add_http_header("Referer".to_string(), url.clone().replace("xs1.php", "index.php"));
|
||||
if let Some(formats) = video_item.formats.as_mut() {
|
||||
formats.push(format);
|
||||
} else {
|
||||
video_item.formats = Some(vec![format]);
|
||||
}
|
||||
items.push(video_item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
impl Provider for PerverzijaProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
_channel: String,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
featured: String,
|
||||
) -> Vec<Video_Item> {
|
||||
let _ = sort;
|
||||
let videos: std::result::Result<Vec<Video_Item>, Error> = match query {
|
||||
Some(q) => self.query(&q),
|
||||
None => self.get(&page.parse::<u8>().unwrap_or(1), featured).await,
|
||||
};
|
||||
match videos {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Error fetching videos: {}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/status.rs
Normal file
122
src/status.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Notice {
|
||||
pub status: String, //"info",
|
||||
pub message: String, //"test message",
|
||||
pub details: String, //"test details",
|
||||
pub priority: bool, //false,
|
||||
pub url: String, //"hottub:\/\/upgrade?source=hottub"
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Channel {
|
||||
pub id: String, //"hottub",
|
||||
pub name: String, //"Hot Tub Pro",
|
||||
pub description: String, //"Advanced search, filters, and more. Consider supporting the development of Hot Tub by upgrading to Hot Tub Pro.",
|
||||
pub premium: bool, //true,
|
||||
pub favicon: String, //"https:\/\/www.google.com/s2/favicons?sz=64&domain=https:\/\/hottubapp.io",
|
||||
pub status: String, //"active",
|
||||
pub categories: Vec<String>, //[],
|
||||
pub options: Vec<Channel_Option>,
|
||||
pub nsfw: bool, //true
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Channel_Option {
|
||||
pub id: String, //"channels",
|
||||
pub title: String, //"Sites",
|
||||
pub description: String, //"Websites included in search results.",
|
||||
pub systemImage: String, //"network",
|
||||
pub colorName: String, //"purple",
|
||||
pub options: Vec<Filter_Option>, //[],
|
||||
pub multiSelect: bool, //true
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Filter_Option{
|
||||
pub id: String, //"sort",
|
||||
pub title: String, //"Sort",
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Options {
|
||||
pub id: String, //"sort",
|
||||
pub title: String, //"Sort",
|
||||
pub description: String, //"Sort the videos by new or old.",
|
||||
pub systemImage: String, //"sort.image",
|
||||
pub colorName: String, //"blue",
|
||||
pub options: Vec<Option_Value>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Option_Value {
|
||||
pub id: String, //"new",
|
||||
pub title: String, //"New",
|
||||
pub description: Option<String>, //"Sort the videos by new or old."
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Subscription {
|
||||
pub status: String, //"incomplete"
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
pub fn new() -> Self {
|
||||
Subscription {
|
||||
status: "incomplete".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Status {
|
||||
pub id: String, //"spacemoehrehottub",
|
||||
pub name: String, //"SpaceMoehre's Hot Tub",
|
||||
pub subtitle: String, //"More sources, more videos, more fun!",
|
||||
pub description: String, //"SpaceMoehre's Hot Tub is a source for finding videos from various websites.",
|
||||
pub iconUrl: String, //"https://hottubapp.io/files/hottub/appicon.png",
|
||||
pub color: String, //"\#A700FF",
|
||||
pub status: String, //"normal",
|
||||
pub notices: Vec<Notice>,
|
||||
pub channels: Vec<Channel>,
|
||||
pub subscription: Subscription,
|
||||
pub nsfw: bool,
|
||||
pub categories: Vec<String>,
|
||||
pub options: Vec<Options>,
|
||||
pub filtersFooter: String, //"Help us improve our algorithms by selecting the categories that best describe you. These will not necessarily affect your search results, but will help us tailor the app to your interests."
|
||||
}
|
||||
impl Status {
|
||||
pub fn new() -> Self {
|
||||
Status {
|
||||
id: "spacemoehre".to_string(),
|
||||
name: "SpaceMoehre's Hot Tub".to_string(),
|
||||
subtitle: "More sources, more videos, more fun!".to_string(),
|
||||
description:
|
||||
"SpaceMoehre's Hot Tub is a source for finding videos from various websites."
|
||||
.to_string(),
|
||||
iconUrl: "https://hottubapp.io/files/hottub/appicon.png".to_string(),
|
||||
color: "#FFA500".to_string(),
|
||||
status: "normal".to_string(),
|
||||
notices: vec![],
|
||||
channels: vec![],
|
||||
subscription: Subscription::new(),
|
||||
nsfw: true,
|
||||
categories: vec![],
|
||||
options: vec![],
|
||||
filtersFooter:
|
||||
"Help us improve our algorithms by giving us your feedback on the hottub discord."
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
pub fn add_notice(&mut self, notice: Notice) {
|
||||
self.notices.push(notice);
|
||||
}
|
||||
pub fn add_channel(&mut self, channel: Channel) {
|
||||
self.channels.push(channel);
|
||||
}
|
||||
pub fn add_option(&mut self, option: Options) {
|
||||
self.options.push(option);
|
||||
}
|
||||
pub fn add_category(&mut self, category: String) {
|
||||
self.categories.push(category);
|
||||
}
|
||||
}
|
||||
1
src/util/mod.rs
Normal file
1
src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod time;
|
||||
19
src/util/time.rs
Normal file
19
src/util/time.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
pub fn parse_time_to_seconds(s: &str) -> Option<i64> {
|
||||
let parts: Vec<_> = s.split(':').collect();
|
||||
match parts.len() {
|
||||
2 => {
|
||||
// MM:SS
|
||||
let minutes: i64 = parts[0].parse().ok()?;
|
||||
let seconds: i64 = parts[1].parse().ok()?;
|
||||
Some(minutes * 60 + seconds)
|
||||
}
|
||||
3 => {
|
||||
// HH:MM:SS
|
||||
let hours: i64 = parts[0].parse().ok()?;
|
||||
let minutes: i64 = parts[1].parse().ok()?;
|
||||
let seconds: i64 = parts[2].parse().ok()?;
|
||||
Some(hours * 3600 + minutes * 60 + seconds)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
152
src/videos.rs
Normal file
152
src/videos.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct Videos_Request {
|
||||
pub channel: Option<String>, //"youtube",
|
||||
pub sort: Option<String>, //"new",
|
||||
pub query: Option<String>, //"kittens",
|
||||
pub page: Option<String>, //1,
|
||||
pub perPage: Option<String>, //10,
|
||||
// Your server's global options will be sent in the videos request
|
||||
// pub flavor: "mint chocolate chip"
|
||||
pub featured: Option<String>, // "featured",
|
||||
}
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct PageInfo {
|
||||
pub hasNextPage: bool, // true,
|
||||
pub resultsPerPage: u32, // 10
|
||||
}
|
||||
|
||||
|
||||
#[derive(serde::Serialize, Debug, Clone)]
|
||||
pub struct Video_Embed{
|
||||
html: String,
|
||||
source: String,
|
||||
}
|
||||
impl Video_Embed {
|
||||
pub fn new(html: String, source: String) -> Self {
|
||||
Video_Embed {
|
||||
html,
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(serde::Serialize, Debug, Clone)]
|
||||
pub struct Video_Item {
|
||||
pub duration: u32, // 110,
|
||||
pub views: Option<u32>, // 14622653,
|
||||
pub rating: Option<f32>, // 0.0,
|
||||
pub id: String, // "c85017ca87477168d648727753c4ded8a35f173e22ef93743e707b296becb299",
|
||||
pub title: String, // "20 Minutes of Adorable Kittens BEST Compilation",
|
||||
pub url: String, // "https://www.youtube.com/watch?v=y0sF5xhGreA",
|
||||
pub channel: String, // "youtube",
|
||||
pub thumb: String, // "https://i.ytimg.com/vi/y0sF5xhGreA/hqdefault.jpg",
|
||||
pub uploader: Option<String>, // "The Pet Collective",
|
||||
pub uploaderUrl: Option<String>, // "https://www.youtube.com/@petcollective",
|
||||
pub verified: Option<bool>, // false,
|
||||
pub tags: Option<Vec<String>>, // [],
|
||||
pub uploadedAt: Option<u64>, // 1741142954
|
||||
pub formats: Option<Vec<Video_Format>>, // Additional HTTP headers if needed
|
||||
pub embed: Option<Video_Embed>, // Optional embed information
|
||||
}
|
||||
impl Video_Item {
|
||||
pub fn new(
|
||||
id: String,
|
||||
title: String,
|
||||
url: String,
|
||||
channel: String,
|
||||
thumb: String,
|
||||
duration: u32,
|
||||
) -> Self {
|
||||
Video_Item {
|
||||
duration: duration, // Placeholder, adjust as needed
|
||||
views: None, // Placeholder, adjust as needed
|
||||
rating: None, // Placeholder, adjust as needed
|
||||
id,
|
||||
title,
|
||||
url,
|
||||
channel,
|
||||
thumb,
|
||||
uploader: None,
|
||||
uploaderUrl: None,
|
||||
verified: None,
|
||||
tags: None, // Placeholder, adjust as needed
|
||||
uploadedAt: None,
|
||||
formats: None, // Placeholder for formats
|
||||
embed: None, // Placeholder for embed information
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, Clone)]
|
||||
pub struct Video_Format {
|
||||
url: String,
|
||||
quality: String,
|
||||
format: String,
|
||||
format_id: Option<String>,
|
||||
format_note: Option<String>,
|
||||
filesize: Option<u32>,
|
||||
asr: Option<u32>,
|
||||
fps: Option<u32>,
|
||||
width: Option<u32>,
|
||||
height: Option<u32>,
|
||||
tbr: Option<u32>,
|
||||
language: Option<String>,
|
||||
language_preference: Option<u32>,
|
||||
ext: Option<String>,
|
||||
vcodec: Option<String>,
|
||||
acodec: Option<String>,
|
||||
dynamic_range: Option<String>,
|
||||
abr: Option<u32>,
|
||||
vbr: Option<u32>,
|
||||
container: Option<String>,
|
||||
protocol: Option<String>,
|
||||
audio_ext: Option<String>,
|
||||
video_ext: Option<String>,
|
||||
resolution: Option<String>,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
}
|
||||
impl Video_Format {
|
||||
pub fn new(url: String, quality: String, format: String) -> Self {
|
||||
Video_Format {
|
||||
url,
|
||||
quality,
|
||||
format,
|
||||
format_id: None,
|
||||
format_note: None,
|
||||
filesize: None,
|
||||
asr: None,
|
||||
fps: None,
|
||||
width: None,
|
||||
height: None,
|
||||
tbr: None,
|
||||
language: None,
|
||||
language_preference: None,
|
||||
ext: None,
|
||||
vcodec: None,
|
||||
acodec: None,
|
||||
dynamic_range: None,
|
||||
abr: None,
|
||||
vbr: None,
|
||||
container: None,
|
||||
protocol: None,
|
||||
audio_ext: None,
|
||||
video_ext: None,
|
||||
resolution: None,
|
||||
http_headers: None,
|
||||
}
|
||||
}
|
||||
pub fn add_http_header(&mut self, key: String, value: String) {
|
||||
if self.http_headers.is_none() {
|
||||
self.http_headers = Some(HashMap::new());
|
||||
}
|
||||
if let Some(headers) = &mut self.http_headers {
|
||||
headers.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct Videos {
|
||||
pub pageInfo: PageInfo,
|
||||
pub items: Vec<Video_Item>,
|
||||
}
|
||||
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Reference in New Issue
Block a user