Compare commits
152 Commits
master
...
97066a184a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97066a184a | ||
|
|
8944646c85 | ||
|
|
0aee46371a | ||
|
|
0ce2347022 | ||
|
|
3feeb02251 | ||
|
|
6b4b0be522 | ||
|
|
bdc26c8b81 | ||
|
|
e7998f8e19 | ||
|
|
4aba459f04 | ||
|
|
b6f6212de0 | ||
|
|
5dd92b21c4 | ||
|
|
37c534f257 | ||
|
|
bbd4f975eb | ||
|
|
62f467ca68 | ||
|
|
32eb704548 | ||
|
|
d1a4975aa3 | ||
|
|
faa2cea37e | ||
|
|
57ed44c2d4 | ||
|
|
f1a3046f62 | ||
|
|
e18e4da559 | ||
|
|
2d1def2dfe | ||
|
|
859ccd5efb | ||
|
|
323fbfd5c9 | ||
|
|
5f084970d2 | ||
|
|
053575f2c3 | ||
|
|
f88129ff39 | ||
|
|
441780f29b | ||
|
|
7d933384c4 | ||
|
|
bbbb8f5fdf | ||
|
|
5806f5ee2b | ||
|
|
44620a88d5 | ||
|
|
624ee7d782 | ||
|
|
9102a9f43f | ||
|
|
519f178dea | ||
|
|
8a477bffc9 | ||
|
|
41374470b1 | ||
|
|
6ef74955cf | ||
|
|
eafd557d09 | ||
|
|
83fe467252 | ||
|
|
3998c8b1a9 | ||
|
|
4c1776bbcb | ||
|
|
31a31f5733 | ||
|
|
28db17a363 | ||
|
|
90f85dc6e8 | ||
|
|
0b2e1478ea | ||
|
|
13c36a4328 | ||
|
|
b4ee574433 | ||
|
|
9d3d8ce67b | ||
|
|
19a6115eb1 | ||
|
|
19146616dc | ||
|
|
9e1a2a65c9 | ||
|
|
7008e38838 | ||
|
|
ae527041ae | ||
|
|
0a60d12525 | ||
|
|
bd565e044a | ||
|
|
a63e260dac | ||
|
|
f81a0e2ec5 | ||
|
|
bed8882329 | ||
|
|
d77e292dbd | ||
|
|
fe8c564126 | ||
|
|
2c38a2fa6e | ||
|
|
853a24f9cd | ||
|
|
4c5e5028da | ||
|
|
0ebfd6cf10 | ||
|
|
465d1fc99c | ||
|
|
93e090c050 | ||
|
|
0d3e0170d4 | ||
|
|
6df8b3e857 | ||
|
|
1d8b79cb76 | ||
|
|
68c566caa7 | ||
|
|
fe542b970d | ||
|
|
3f391a4516 | ||
|
|
9cf532e831 | ||
|
|
b7a3daebe3 | ||
|
|
97617735e4 | ||
|
|
3c9c9c8cd3 | ||
|
|
d663b344aa | ||
|
|
e1735657f0 | ||
|
|
0a5adac63a | ||
|
|
b94fca9986 | ||
|
|
026266dd83 | ||
|
|
242ce91525 | ||
|
|
23f6df62f0 | ||
|
|
6405cbb269 | ||
|
|
f8fe0aa1ec | ||
|
|
842db68c57 | ||
|
|
c34d6dcc14 | ||
|
|
8cd404d6b1 | ||
|
|
2a912a4010 | ||
|
|
9bec5e4b60 | ||
|
|
0405d2a5ce | ||
|
|
15c8a93990 | ||
|
|
727ceaef4b | ||
|
|
5f4c12e2ff | ||
|
|
a7a107c9b4 | ||
|
|
00b45ecaf9 | ||
|
|
b8423f6731 | ||
|
|
61cf3f625e | ||
|
|
673d9aad5b | ||
|
|
0496954f41 | ||
|
|
578ac3e034 | ||
|
|
f4f22572c1 | ||
|
|
e87a2ed237 | ||
|
|
95eeb273f5 | ||
|
|
69301f1e97 | ||
|
|
ec1d7b8eef | ||
|
|
60a07269f6 | ||
|
|
df323ec9fd | ||
|
|
175c9b748f | ||
|
|
6d08362937 | ||
|
|
52081698e9 | ||
|
|
d837028faf | ||
|
|
cb03417f5f | ||
|
|
d7fc427696 | ||
|
|
3150e57411 | ||
|
|
8d5da3a4dc | ||
|
|
2ddc5e86e2 | ||
|
|
2e8b8bea0c | ||
|
|
082b3b5c1d | ||
|
|
a7610e1bb3 | ||
|
|
261c81e391 | ||
|
|
1324d58f50 | ||
|
|
9399949c36 | ||
|
|
03e4554131 | ||
|
|
c218828d40 | ||
|
|
15c5216309 | ||
|
|
58cff87274 | ||
|
|
e51de99853 | ||
|
|
6b1746180f | ||
|
|
08d7b09e05 | ||
|
|
d74b7b97e6 | ||
|
|
d1b23dd293 | ||
|
|
0f9c23168c | ||
|
|
4cd9661d4b | ||
|
|
91afe6e48f | ||
|
|
ae312a83fb | ||
|
|
4cf29ce201 | ||
|
|
8da7b30c07 | ||
|
|
cae15e7636 | ||
|
|
d2254128d7 | ||
|
|
be83e12bc3 | ||
|
|
babaf90762 | ||
|
|
860eadcbd4 | ||
|
|
ae8fd8e922 | ||
|
|
918ed1a125 | ||
|
|
edc7879324 | ||
|
|
580751af03 | ||
|
|
3fe699b62d | ||
|
|
0cb3531ae4 | ||
|
|
5b9a1b351c | ||
| 20bf6b745b | |||
| 7fa6bdeb3c |
3
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
.testing/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
@@ -20,3 +21,5 @@ Cargo.lock
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
hottub.db
|
||||
migrations/.keep
|
||||
|
||||
16
Cargo.toml
@@ -1,19 +1,21 @@
|
||||
[package]
|
||||
name = "hottub"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.88"
|
||||
awc = "3.7.0"
|
||||
cute = "0.3.0"
|
||||
diesel = { version = "2.2.10", features = ["sqlite", "r2d2"] }
|
||||
dotenvy = "0.15.7"
|
||||
env_logger = "0.11.8"
|
||||
error-chain = "0.12.4"
|
||||
futures = "0.3.31"
|
||||
htmlentity = "1.3.2"
|
||||
ntex = { version = "2.0", features = ["tokio", "openssl"] }
|
||||
ntex = { version = "2.0", features = ["tokio"] }
|
||||
ntex-files = "2.0.0"
|
||||
once_cell = "1.21.3"
|
||||
reqwest = { version = "0.12.18", features = ["blocking", "json", "rustls-tls"] }
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
wreq = { version = "5", features = ["full"] }
|
||||
wreq-util = "2"
|
||||
percent-encoding = "2.1"
|
||||
32
Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM debian
|
||||
# FROM consol/debian-xfce-vnc:latest
|
||||
# Switch to root user to install additional software
|
||||
USER 0
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -yq libssl-dev \
|
||||
wget curl unzip \
|
||||
openssl \
|
||||
ca-certificates \
|
||||
fontconfig \
|
||||
fonts-dejavu \
|
||||
libxext6 \
|
||||
libxrender1 \
|
||||
libxtst6 \
|
||||
gnupg \
|
||||
supervisor \
|
||||
python3 python3-pip python3-venv\
|
||||
scrot python3-tk python3-dev \
|
||||
libx11-6 libx11-dev libxext-dev libxtst6 \
|
||||
libpng-dev libjpeg-dev libtiff-dev libfreetype6-dev \
|
||||
x11-xserver-utils \
|
||||
xserver-xorg \
|
||||
fluxbox \
|
||||
xvfb \
|
||||
gnome-screenshot \
|
||||
libsqlite3-dev sqlite3 sqlitebrowser \
|
||||
sudo \
|
||||
&& apt-get clean
|
||||
|
||||
USER 1000
|
||||
|
||||
BIN
burp/accept.png
Normal file
|
After Width: | Height: | Size: 698 B |
BIN
burp/close.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
burp/http_history.png
Normal file
|
After Width: | Height: | Size: 780 B |
BIN
burp/next_button.png
Normal file
|
After Width: | Height: | Size: 526 B |
20
burp/project_options.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"proxy":{
|
||||
"request_listeners":[
|
||||
{
|
||||
"certificate_mode":"per_host",
|
||||
"custom_tls_protocols":[
|
||||
"SSLv3",
|
||||
"TLSv1",
|
||||
"TLSv1.1",
|
||||
"TLSv1.2",
|
||||
"TLSv1.3"
|
||||
],
|
||||
"listen_mode":"all_interfaces",
|
||||
"listener_port":8080,
|
||||
"running":true,
|
||||
"use_custom_tls_protocols":false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
burp/proxy.png
Normal file
|
After Width: | Height: | Size: 780 B |
BIN
burp/sort.png
Normal file
|
After Width: | Height: | Size: 379 B |
BIN
burp/start_burp.png
Normal file
|
After Width: | Height: | Size: 818 B |
98
burp/start_burp.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import pyautogui
|
||||
import time
|
||||
import os
|
||||
import subprocess
|
||||
import datetime
|
||||
|
||||
BURP_JAR = "/headless/burpsuite_community.jar"
|
||||
CONFIG_FILE = "/app/burp/project_options.json"
|
||||
|
||||
def start_burp():
|
||||
os.system("rm -rf /tmp/burp*")
|
||||
burp_process = subprocess.Popen([
|
||||
"java", "-jar", BURP_JAR,
|
||||
f"--config-file={CONFIG_FILE}"
|
||||
])
|
||||
return burp_process
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
print("Starting Burp Suite...")
|
||||
burp_process = start_burp()
|
||||
end_time = datetime.datetime.now() + datetime.timedelta(days=1)
|
||||
button = None
|
||||
proxy_clicked = False
|
||||
history_clicked = False
|
||||
sort_clicked = False
|
||||
setup = False
|
||||
while True:
|
||||
if datetime.datetime.now() > end_time:
|
||||
|
||||
setup = False
|
||||
print("Burp Suite has been running for 24 hours, restarting...")
|
||||
burp_process.terminate()
|
||||
time.sleep(1)
|
||||
burp_process = start_burp()
|
||||
end_time = datetime.datetime.now() + datetime.timedelta(days=1)
|
||||
proxy_clicked = False
|
||||
history_clicked = False
|
||||
sort_clicked = False
|
||||
if not setup:
|
||||
try:
|
||||
button = pyautogui.locateCenterOnScreen("/app/burp/next_button.png", confidence=0.8)
|
||||
except:
|
||||
pass
|
||||
if button:
|
||||
print("Clicking on the 'Next' button...")
|
||||
pyautogui.click(button)
|
||||
button = None
|
||||
|
||||
try:
|
||||
button = pyautogui.locateCenterOnScreen("/app/burp/start_burp.png", confidence=0.8)
|
||||
except:
|
||||
pass
|
||||
if button:
|
||||
print("Clicking on the 'Start Burp' button...")
|
||||
pyautogui.click(button)
|
||||
button = None
|
||||
|
||||
try:
|
||||
button = pyautogui.locateCenterOnScreen("/app/burp/accept.png", confidence=0.8)
|
||||
except:
|
||||
pass
|
||||
if button:
|
||||
print("Clicking on the 'Accept' button...")
|
||||
pyautogui.click(button)
|
||||
button = None
|
||||
|
||||
try:
|
||||
button = pyautogui.locateCenterOnScreen("/app/burp/proxy.png", confidence=0.8)
|
||||
except:
|
||||
pass
|
||||
if button and not proxy_clicked:
|
||||
print("Clicking on the 'Proxy' button...")
|
||||
pyautogui.click(button)
|
||||
proxy_clicked = True
|
||||
button = None
|
||||
|
||||
try:
|
||||
button = pyautogui.locateCenterOnScreen("/app/burp/http_history.png", confidence=0.8)
|
||||
except:
|
||||
pass
|
||||
if button and not history_clicked:
|
||||
print("Clicking on the 'HTTP History' button...")
|
||||
pyautogui.click(button)
|
||||
history_clicked = True
|
||||
button = None
|
||||
try:
|
||||
button = pyautogui.locateCenterOnScreen("/app/burp/sort.png", confidence=0.99)
|
||||
except:
|
||||
pass
|
||||
if button and not sort_clicked:
|
||||
sort_clicked = True
|
||||
print("Clicking on the 'Sorting' button...")
|
||||
pyautogui.click(button)
|
||||
setup = True
|
||||
button = None
|
||||
else:
|
||||
time.sleep(3600)
|
||||
9
diesel.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
||||
23
docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
hottub:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: hottub
|
||||
entrypoint: supervisord
|
||||
command: ["-c", "/app/supervisord/supervisord.conf"]
|
||||
volumes:
|
||||
- /path/to/hottub:/app
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
- BURP_URL=http://127.0.0.1:8081
|
||||
restart: unless-stopped
|
||||
working_dir: /app
|
||||
ports:
|
||||
- 6901:6901
|
||||
- 8080:18080
|
||||
|
||||
|
||||
networks:
|
||||
traefik_default:
|
||||
external: true
|
||||
2
migrations/create_videos/down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE videos
|
||||
8
migrations/create_videos/up.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Your SQL goes here
|
||||
CREATE TABLE videos (
|
||||
id TEXT NOT NULL PRIMARY KEY, -- like url parts to uniquely identify a video
|
||||
url TEXT NOT NULL--,
|
||||
--views INTEGER,
|
||||
--rating INTEGER,
|
||||
--uploader TEXT
|
||||
)
|
||||
708
src/api.rs
@@ -1,18 +1,93 @@
|
||||
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 std::cmp::Ordering;
|
||||
use tokio::task;
|
||||
|
||||
use crate::providers::all::AllProvider;
|
||||
use crate::providers::hanime::HanimeProvider;
|
||||
use crate::providers::perverzija::PerverzijaProvider;
|
||||
use crate::{providers::*, status::*, videos::*};
|
||||
use crate::providers::pmvhaven::PmvhavenProvider;
|
||||
use crate::providers::pornhub::PornhubProvider;
|
||||
use crate::providers::redtube::RedtubeProvider;
|
||||
use crate::providers::rule34video::Rule34videoProvider;
|
||||
use crate::providers::spankbang::SpankbangProvider;
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::{DbPool, providers::*, status::*, videos::*};
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
|
||||
// this function could be located in a different module
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("/status")
|
||||
@@ -27,6 +102,15 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -35,134 +119,377 @@ async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
||||
.to_string();
|
||||
let mut status = Status::new();
|
||||
|
||||
// You can now use `method`, `host`, and `port` as needed
|
||||
|
||||
// pronhub
|
||||
status.add_channel(Channel {
|
||||
id: "all".to_string(),
|
||||
name: "SpaceMoehre's Hottub".to_string(),
|
||||
favicon: format!("http://{}/static/favicon.ico", host).to_string(),
|
||||
id: "pornhub".to_string(),
|
||||
name: "Pornhub".to_string(),
|
||||
description: "Pornhub Free Videos".to_string(),
|
||||
premium: false,
|
||||
description: "Work in Progress".to_string(),
|
||||
favicon: "https://www.google.com/s2/favicons?sz=64&domain=pornhub.com".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,
|
||||
},
|
||||
],
|
||||
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: "mr".to_string(),
|
||||
title: "Most Recent".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "mv".to_string(),
|
||||
title: "Most Viewed".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "tr".to_string(),
|
||||
title: "Top Rated".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "lg".to_string(),
|
||||
title: "Longest".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(),
|
||||
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![
|
||||
Filter_Option {
|
||||
id: "date".to_string(),
|
||||
title: "Date".to_string(),
|
||||
FilterOption {
|
||||
id: "all".to_string(),
|
||||
title: "All".to_string(),
|
||||
},
|
||||
Filter_Option {
|
||||
id: "name".to_string(),
|
||||
title: "Name".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,
|
||||
},
|
||||
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(),
|
||||
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![
|
||||
Filter_Option {
|
||||
id: "short".to_string(),
|
||||
title: "< 1h".to_string(),
|
||||
FilterOption {
|
||||
id: "Newest".to_string(),
|
||||
title: "Newest".to_string(),
|
||||
},
|
||||
Filter_Option {
|
||||
id: "long".to_string(),
|
||||
title: "> 1h".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: true,
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
}],
|
||||
nsfw: true,
|
||||
});
|
||||
}
|
||||
if clientversion >= ClientVersion::new(22, 97, "22a".to_string()) {
|
||||
// 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: "spankbang".to_string(),
|
||||
// name: "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.add_channel(Channel {
|
||||
id: "rule34video".to_string(),
|
||||
name: "Rule34Video".to_string(),
|
||||
description: "If it exists, there is porn".to_string(),
|
||||
premium: false,
|
||||
favicon: "https://www.google.com/s2/favicons?sz=64&domain=rule34video.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: "post_date".to_string(),
|
||||
title: "Newest".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "video_viewed".to_string(),
|
||||
title: "Most Viewed".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "rating".to_string(),
|
||||
title: "Top Rated".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "duration".to_string(),
|
||||
title: "Longest".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "pseudo_random".to_string(),
|
||||
title: "Random".to_string(),
|
||||
},
|
||||
],
|
||||
multiSelect: false,
|
||||
}],
|
||||
nsfw: true,
|
||||
});
|
||||
|
||||
// All
|
||||
status.add_channel(Channel {
|
||||
id: "all".to_string(),
|
||||
name: "All".to_string(),
|
||||
description: "(Work in Progress) Query from all sites of this Server".to_string(),
|
||||
premium: false,
|
||||
favicon: "https://hottub.spacemoehre.de/favicon.ico".to_string(),
|
||||
status: "active".to_string(),
|
||||
categories: vec![],
|
||||
options: vec![ChannelOption {
|
||||
id: "sites".to_string(),
|
||||
title: "Sites".to_string(),
|
||||
description: "What Sites to use".to_string(), //"Sort the videos by Date or Name.".to_string(),
|
||||
systemImage: "list.number".to_string(),
|
||||
colorName: "green".to_string(),
|
||||
options: vec![
|
||||
FilterOption {
|
||||
id: "hanime".to_string(),
|
||||
title: "Hanime".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "perverzija".to_string(),
|
||||
title: "Perverzija".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "pmvhaven".to_string(),
|
||||
title: "PMVHaven".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "pornhub".to_string(),
|
||||
title: "Pornhub".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "redtube".to_string(),
|
||||
title: "Redtube".to_string(),
|
||||
},
|
||||
FilterOption {
|
||||
id: "rule34video".to_string(),
|
||||
title: "Rule34Video".to_string(),
|
||||
},
|
||||
],
|
||||
multiSelect: true,
|
||||
}],
|
||||
nsfw: true,
|
||||
});
|
||||
|
||||
status.add_channel(Channel {
|
||||
id: "redtube".to_string(),
|
||||
name: "Redtube".to_string(),
|
||||
description: "Redtube brings you NEW porn videos every day for free".to_string(),
|
||||
premium: false,
|
||||
favicon: "https://www.google.com/s2/favicons?sz=64&domain=www.redtube.com".to_string(),
|
||||
status: "active".to_string(),
|
||||
categories: vec![],
|
||||
options: vec![],
|
||||
nsfw: true,
|
||||
});
|
||||
status.iconUrl = format!("http://{}/favicon.ico", host).to_string();
|
||||
@@ -170,19 +497,10 @@ async fn status(req: HttpRequest) -> Result<impl web::Responder, web::Error> {
|
||||
}
|
||||
|
||||
async fn videos_post(
|
||||
video_request: web::types::Json<Videos_Request>,
|
||||
video_request: web::types::Json<VideosRequest>,
|
||||
cache: web::types::State<VideoCache>,
|
||||
pool: web::types::State<DbPool>,
|
||||
) -> 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,
|
||||
@@ -214,61 +532,83 @@ async fn videos_post(
|
||||
.to_string()
|
||||
.parse()
|
||||
.unwrap();
|
||||
let featured = video_request.featured.as_deref().unwrap_or("all").to_string();
|
||||
let provider = PerverzijaProvider::new();
|
||||
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 category = video_request
|
||||
.category
|
||||
.as_deref()
|
||||
.unwrap_or("all")
|
||||
.to_string();
|
||||
let sites = video_request
|
||||
.sites
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let options = ServerOptions { featured: Some(featured), category: Some(category), sites: Some(sites) };
|
||||
let video_items = provider
|
||||
.get_videos(channel, sort, query, page.to_string(), perPage.to_string(), featured)
|
||||
.get_videos(
|
||||
cache.get_ref().clone(),
|
||||
pool.get_ref().clone(),
|
||||
sort.clone(),
|
||||
query.clone(),
|
||||
page.to_string(),
|
||||
perPage.to_string(),
|
||||
options.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 sort_clone = sort.clone();
|
||||
let query_clone = query.clone();
|
||||
let per_page_clone = perPage.to_string();
|
||||
let options_clone = options.clone();
|
||||
task::spawn_local(async move {
|
||||
// if let AnyProvider::Spankbang(_) = provider_clone {
|
||||
// // Spankbang has a delay for the next page
|
||||
// ntex::time::sleep(ntex::time::Seconds(80)).await;
|
||||
// }
|
||||
let _ = provider_clone
|
||||
.get_videos(
|
||||
cache_clone,
|
||||
pool_clone,
|
||||
sort_clone,
|
||||
query_clone,
|
||||
next_page.to_string(),
|
||||
per_page_clone,
|
||||
options_clone
|
||||
)
|
||||
.await;
|
||||
});
|
||||
//###
|
||||
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
|
||||
pub fn get_provider(channel: &str) -> Option<AnyProvider> {
|
||||
match channel {
|
||||
"all" => Some(AnyProvider::All(AllProvider::new())),
|
||||
"perverzija" => Some(AnyProvider::Perverzija(PerverzijaProvider::new())),
|
||||
"hanime" => Some(AnyProvider::Hanime(HanimeProvider::new())),
|
||||
"spankbang" => Some(AnyProvider::Spankbang(SpankbangProvider::new())),
|
||||
"pornhub" => Some(AnyProvider::Pornhub(PornhubProvider::new())),
|
||||
"pmvhaven" => Some(AnyProvider::Pmvhaven(PmvhavenProvider::new())),
|
||||
"rule34video" => Some(AnyProvider::Rule34video(Rule34videoProvider::new())),
|
||||
"redtube" => Some(AnyProvider::Redtube(RedtubeProvider::new())),
|
||||
|
||||
// }
|
||||
// ],
|
||||
// };
|
||||
|
||||
// println!("Video: {:?}", videos);
|
||||
// Ok(web::HttpResponse::Ok().json(&videos))
|
||||
// }
|
||||
_ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())),
|
||||
}
|
||||
}
|
||||
|
||||
29
src/db.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use diesel::prelude::*;
|
||||
use crate::models::DBVideo;
|
||||
|
||||
|
||||
pub fn get_video(conn: &mut SqliteConnection, video_id: String) -> Result<Option<String>, diesel::result::Error> {
|
||||
use crate::schema::videos::dsl::*;
|
||||
let result = videos
|
||||
.filter(id.eq(video_id))
|
||||
.first::<DBVideo>(conn)
|
||||
.optional()?;
|
||||
match result{
|
||||
Some(video) => Ok(Some(video.url)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_video(conn: &mut SqliteConnection, new_id: &str, new_url: &str) -> Result<usize, diesel::result::Error> {
|
||||
use crate::schema::videos::dsl::*;
|
||||
diesel::insert_into(videos).values(DBVideo{
|
||||
id: new_id.to_string(),
|
||||
url: new_url.to_string(),
|
||||
}).execute(conn)
|
||||
}
|
||||
|
||||
pub fn delete_video(conn: &mut SqliteConnection, video_id: String) -> Result<usize, diesel::result::Error> {
|
||||
use crate::schema::videos::dsl::*;
|
||||
diesel::delete(videos.filter(id.eq(video_id))).execute(conn)
|
||||
}
|
||||
|
||||
56
src/main.rs
@@ -1,30 +1,62 @@
|
||||
use ntex_files as fs;
|
||||
#![warn(unused_extern_crates)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
|
||||
use diesel::{r2d2::{self, ConnectionManager}, SqliteConnection};
|
||||
use dotenvy::dotenv;
|
||||
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 db;
|
||||
mod models;
|
||||
mod providers;
|
||||
mod schema;
|
||||
mod status;
|
||||
mod util;
|
||||
mod videos;
|
||||
|
||||
type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
||||
|
||||
|
||||
|
||||
#[ntex::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "ntex=warn");
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
// std::env::set_var("RUST_BACKTRACE", "1");
|
||||
dotenv().ok();
|
||||
|
||||
// Enable request logging
|
||||
unsafe {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
env_logger::init(); // You need this to actually see logs
|
||||
|
||||
// set up database connection pool
|
||||
let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL");
|
||||
let manager = ConnectionManager::<SqliteConnection>::new(connspec);
|
||||
let pool = r2d2::Pool::builder()
|
||||
.build(manager)
|
||||
.expect("Failed to create pool.");
|
||||
|
||||
web::HttpServer::new(|| {
|
||||
let cache: util::cache::VideoCache = crate::util::cache::VideoCache::new();
|
||||
|
||||
web::HttpServer::new(move || {
|
||||
web::App::new()
|
||||
.state(pool.clone())
|
||||
.state(cache.clone())
|
||||
.wrap(web::middleware::Logger::default())
|
||||
.service(web::scope("/api").configure(api::config))
|
||||
.service(fs::Files::new("/", "static"))
|
||||
.service(
|
||||
web::resource("/")
|
||||
.route(web::get().to(|| async {
|
||||
web::HttpResponse::Found()
|
||||
.header("Location", "hottub://source?url=hottub.spacemoehre.de")
|
||||
.finish()
|
||||
}))
|
||||
)
|
||||
.service(fs::Files::new("/", "static").index_file("index.html"))
|
||||
})
|
||||
.workers(8)
|
||||
// .bind_openssl(("0.0.0.0", 18080), builder)?
|
||||
.bind(("0.0.0.0", 18080))?
|
||||
.run()
|
||||
|
||||
10
src/models.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use diesel::prelude::*;
|
||||
use serde::{Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Queryable, Insertable)]
|
||||
#[diesel(table_name = crate::schema::videos)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct DBVideo {
|
||||
pub id: String,
|
||||
pub url: String,
|
||||
}
|
||||
72
src/providers/all.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::vec;
|
||||
use error_chain::error_chain;
|
||||
use futures::future::join_all;
|
||||
use serde_json::error::Category;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
use crate::api::get_provider;
|
||||
use crate::db;
|
||||
use crate::providers::{AnyProvider, Provider};
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::util::interleave;
|
||||
use crate::videos::{self, ServerOptions, VideoItem};
|
||||
use crate::DbPool;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
HttpRequest(wreq::Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct AllProvider {
|
||||
}
|
||||
|
||||
impl AllProvider {
|
||||
pub fn new() -> Self {
|
||||
AllProvider {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for AllProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
options: ServerOptions,
|
||||
) -> Vec<VideoItem> {
|
||||
let mut sites_str = options.clone().sites.unwrap();
|
||||
if sites_str.is_empty() {
|
||||
sites_str = "perverzija,hanime,spankbang,pmvhaven,redtube,pornhub,rule34video".to_string();
|
||||
}
|
||||
let sites = sites_str
|
||||
.split(',')
|
||||
.map(|s| s.to_string()) // or s.to_owned()
|
||||
.collect::<Vec<String>>();
|
||||
let providers = sites.iter().map(|el| get_provider(el.as_str()).unwrap()).collect::<Vec<AnyProvider>>();
|
||||
|
||||
let futures = providers.iter().map(|provider| {
|
||||
provider.get_videos(
|
||||
cache.clone(),
|
||||
pool.clone(),
|
||||
sort.clone(),
|
||||
query.clone(),
|
||||
page.clone(),
|
||||
per_page.clone(),
|
||||
options.clone()
|
||||
)
|
||||
}).collect::<Vec<_>>();
|
||||
let results:Vec<Vec<VideoItem>> = join_all(futures).await;
|
||||
let video_items: Vec<VideoItem> = interleave(&results);
|
||||
|
||||
|
||||
return video_items;
|
||||
}
|
||||
}
|
||||
283
src/providers/hanime.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use std::vec;
|
||||
use error_chain::error_chain;
|
||||
use futures::future::join_all;
|
||||
use serde_json::error::Category;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
use crate::db;
|
||||
use crate::providers::Provider;
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::videos::{self, ServerOptions, VideoItem};
|
||||
use crate::DbPool;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
HttpRequest(wreq::Error);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
struct HanimeSearchRequest{
|
||||
search_text: String,
|
||||
tags: Vec<String>,
|
||||
tags_mode: String,
|
||||
brands: Vec<String>,
|
||||
blacklist: Vec<String>,
|
||||
order_by: String,
|
||||
ordering: String,
|
||||
page: u8
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl HanimeSearchRequest {
|
||||
pub fn new() -> Self {
|
||||
HanimeSearchRequest {
|
||||
search_text: "".to_string(),
|
||||
tags: vec![],
|
||||
tags_mode: "AND".to_string(),
|
||||
brands: vec![],
|
||||
blacklist: vec![],
|
||||
order_by: "created_at_unix".to_string(),
|
||||
ordering: "desc".to_string(),
|
||||
page: 0
|
||||
}
|
||||
}
|
||||
pub fn tags(mut self, tags: Vec<String>) -> Self {
|
||||
self.tags = tags;
|
||||
self
|
||||
}
|
||||
pub fn search_text(mut self, search_text: String) -> Self {
|
||||
self.search_text = search_text;
|
||||
self
|
||||
}
|
||||
pub fn tags_mode(mut self, tags_mode: String) -> Self {
|
||||
self.tags_mode = tags_mode;
|
||||
self
|
||||
}
|
||||
pub fn brands(mut self, brands: Vec<String>) -> Self {
|
||||
self.brands = brands;
|
||||
self
|
||||
}
|
||||
pub fn blacklist(mut self, blacklist: Vec<String>) -> Self {
|
||||
self.blacklist = blacklist;
|
||||
self
|
||||
}
|
||||
pub fn order_by(mut self, order_by: String) -> Self {
|
||||
self.order_by = order_by;
|
||||
self
|
||||
}
|
||||
pub fn ordering(mut self, ordering: String) -> Self {
|
||||
self.ordering = ordering;
|
||||
self
|
||||
}
|
||||
pub fn page(mut self, page: u8) -> Self {
|
||||
self.page = page;
|
||||
self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
struct HanimeSearchResponse{
|
||||
page: u8,
|
||||
nbPages:u8,
|
||||
nbHits: u32,
|
||||
hitsPerPage: u8,
|
||||
hits: String
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
struct HanimeSearchResult{
|
||||
id: u64,
|
||||
name: String,
|
||||
titles: Vec<String>,
|
||||
slug: String,
|
||||
description: String,
|
||||
views: u64,
|
||||
interests: u64,
|
||||
poster_url: String,
|
||||
cover_url: String,
|
||||
brand: String,
|
||||
brand_id: u64,
|
||||
duration_in_ms: u32,
|
||||
is_censored: bool,
|
||||
rating: Option<u32>,
|
||||
likes: u64,
|
||||
dislikes: u64,
|
||||
downloads: u64,
|
||||
monthly_ranked: Option<u64>,
|
||||
tags: Vec<String>,
|
||||
created_at: u64,
|
||||
released_at: u64,
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct HanimeProvider {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl HanimeProvider {
|
||||
pub fn new() -> Self {
|
||||
HanimeProvider {
|
||||
url: "https://hanime.tv/".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_video_item(&self, hit: HanimeSearchResult, pool: DbPool) -> Result<VideoItem> {
|
||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||
let db_result = db::get_video(&mut conn,format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug.clone()));
|
||||
drop(conn);
|
||||
let id = hit.id.to_string();
|
||||
let title = hit.name;
|
||||
let thumb = hit.poster_url;
|
||||
let duration = (hit.duration_in_ms / 1000) as u32; // Convert ms to seconds
|
||||
let channel = "hanime".to_string(); // Placeholder, adjust as needed
|
||||
match db_result {
|
||||
Ok(Some(video_url)) => {
|
||||
return Ok(VideoItem::new(id, title, video_url.clone(), channel, thumb, duration)
|
||||
.tags(hit.tags)
|
||||
.uploader(hit.brand)
|
||||
.views(hit.views as u32)
|
||||
.rating((hit.likes as f32 / (hit.likes + hit.dislikes)as f32) * 100 as f32)
|
||||
.formats(vec![videos::VideoFormat::new(video_url.clone(), "1080".to_string(), "m3u8".to_string())]));
|
||||
}
|
||||
Ok(None) => (),
|
||||
Err(e) => {
|
||||
println!("Error fetching video from database: {}", e);
|
||||
// return Err(format!("Error fetching video from database: {}", e).into());
|
||||
}
|
||||
}
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox136)
|
||||
.build()?;
|
||||
let url = format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug);
|
||||
let response = client.get(url).send().await?;
|
||||
|
||||
let text = match response.status().is_success() {
|
||||
true => {
|
||||
response.text().await?
|
||||
},
|
||||
false => {
|
||||
print!("Failed to fetch video item: {}\n\n", response.status());
|
||||
return Err(format!("Failed to fetch video item: {}", response.status()).into());
|
||||
}
|
||||
};
|
||||
|
||||
let urls = text.split("\"servers\"").collect::<Vec<&str>>()[1];
|
||||
let mut url_vec = vec![];
|
||||
|
||||
for el in urls.split("\"url\":\"").collect::<Vec<&str>>(){
|
||||
let url = el.split("\"").collect::<Vec<&str>>()[0];
|
||||
if !url.is_empty() && url.contains("m3u8") {
|
||||
url_vec.push(url.to_string());
|
||||
}
|
||||
}
|
||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||
let _ = db::insert_video(&mut conn, &format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug.clone()), &url_vec[0].clone());
|
||||
drop(conn);
|
||||
Ok(VideoItem::new(id, title, url_vec[0].clone(), channel, thumb, duration)
|
||||
.tags(hit.tags)
|
||||
.uploader(hit.brand)
|
||||
.views(hit.views as u32)
|
||||
.rating((hit.likes as f32 / (hit.likes + hit.dislikes)as f32) * 100 as f32)
|
||||
.formats(vec![videos::VideoFormat::new(url_vec[0].clone(), "1080".to_string(), "m3u8".to_string())]))
|
||||
|
||||
}
|
||||
|
||||
async fn get(&self, cache: VideoCache, pool: DbPool, page: u8, query: String, sort:String) -> Result<Vec<VideoItem>> {
|
||||
let index = format!("hanime:{}:{}:{}", query, page, sort);
|
||||
let order_by = match sort.contains("."){
|
||||
true => sort.split(".").collect::<Vec<&str>>()[0].to_string(),
|
||||
false => "created_at_unix".to_string(),
|
||||
};
|
||||
let ordering = match sort.contains("."){
|
||||
true => sort.split(".").collect::<Vec<&str>>()[1].to_string(),
|
||||
false => "desc".to_string(),
|
||||
};
|
||||
let old_items = match cache.get(&index) {
|
||||
Some((time, items)) => {
|
||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 * 12 {
|
||||
println!("Cache hit for URL: {}", index);
|
||||
return Ok(items.clone());
|
||||
}
|
||||
else{
|
||||
items.clone()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
let search = HanimeSearchRequest::new()
|
||||
.page(page-1)
|
||||
.search_text(query.clone())
|
||||
.order_by(order_by)
|
||||
.ordering(ordering);
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox136)
|
||||
.build()?;
|
||||
let response = client.post("https://search.htv-services.com/search")
|
||||
.json(&search)
|
||||
.send().await?;
|
||||
|
||||
|
||||
|
||||
let hits = match response.json::<HanimeSearchResponse>().await {
|
||||
Ok(resp) => resp.hits,
|
||||
Err(e) => {
|
||||
println!("Failed to parse HanimeSearchResponse: {}", e);
|
||||
return Ok(old_items);
|
||||
}
|
||||
};
|
||||
let hits_json: Vec<HanimeSearchResult> = serde_json::from_str(hits.as_str())
|
||||
.map_err(|e| format!("Failed to parse hits JSON: {}", e))?;
|
||||
// let timeout_duration = Duration::from_secs(120);
|
||||
let futures = hits_json.into_iter().map(|el| self.get_video_item(el.clone(), pool.clone()));
|
||||
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
||||
let video_items: Vec<VideoItem> = results
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&index);
|
||||
cache.insert(index.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
|
||||
Ok(video_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for HanimeProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
options: ServerOptions,
|
||||
) -> Vec<VideoItem> {
|
||||
let _ = options;
|
||||
let _ = per_page;
|
||||
let _ = sort;
|
||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||
Some(q) => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), q, sort).await,
|
||||
None => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), "".to_string(), sort).await,
|
||||
};
|
||||
match videos {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Error fetching videos: {}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,99 @@
|
||||
use crate::videos::{Video_Item, Videos};
|
||||
use crate::{
|
||||
providers::{
|
||||
all::AllProvider, hanime::HanimeProvider, perverzija::PerverzijaProvider, pmvhaven::PmvhavenProvider, pornhub::PornhubProvider, redtube::RedtubeProvider, rule34video::Rule34videoProvider, spankbang::SpankbangProvider
|
||||
}, util::cache::VideoCache, videos::{ServerOptions, VideoItem}, DbPool
|
||||
};
|
||||
|
||||
pub mod all;
|
||||
pub mod hanime;
|
||||
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>;
|
||||
pub mod pmvhaven;
|
||||
pub mod pornhub;
|
||||
pub mod spankbang;
|
||||
pub mod rule34video;
|
||||
pub mod redtube;
|
||||
|
||||
pub trait Provider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
options: ServerOptions,
|
||||
) -> Vec<VideoItem>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AnyProvider {
|
||||
All(AllProvider),
|
||||
Perverzija(PerverzijaProvider),
|
||||
Hanime(HanimeProvider),
|
||||
Spankbang(SpankbangProvider),
|
||||
Pornhub(PornhubProvider),
|
||||
Pmvhaven(PmvhavenProvider),
|
||||
Rule34video(Rule34videoProvider),
|
||||
Redtube(RedtubeProvider), // Assuming Redtube is similar to Rule34video
|
||||
}
|
||||
|
||||
impl Provider for AnyProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
options: ServerOptions
|
||||
) -> Vec<VideoItem> {
|
||||
println!(
|
||||
"/api/videos: sort={:?}, query={:?}, page={:?}, provider={:?}",
|
||||
sort, query, page, self
|
||||
);
|
||||
match self {
|
||||
AnyProvider::Perverzija(p) => {
|
||||
p.get_videos(
|
||||
cache.clone(),
|
||||
pool.clone(),
|
||||
sort.clone(),
|
||||
query.clone(),
|
||||
page.clone(),
|
||||
per_page.clone(),
|
||||
options,
|
||||
)
|
||||
.await
|
||||
}
|
||||
AnyProvider::Hanime(p) => {
|
||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
||||
.await
|
||||
}
|
||||
AnyProvider::Spankbang(p) => {
|
||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
||||
.await
|
||||
}
|
||||
AnyProvider::Pornhub(p) => {
|
||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
||||
.await
|
||||
}
|
||||
AnyProvider::Pmvhaven(p) => {
|
||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
||||
.await
|
||||
}
|
||||
AnyProvider::Rule34video(p) => {
|
||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
||||
.await
|
||||
}
|
||||
AnyProvider::Redtube(p) => {
|
||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
||||
.await
|
||||
}
|
||||
AnyProvider::All(p) => {
|
||||
p.get_videos(cache, pool, sort, query, page, per_page, options,)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,40 @@
|
||||
use std::vec;
|
||||
|
||||
use std::env;
|
||||
use error_chain::error_chain;
|
||||
use htmlentity::entity::{decode, encode, CharacterSet, EncodeType, ICodedDataTrait};
|
||||
use htmlentity::types::{AnyhowResult, Byte};
|
||||
|
||||
use htmlentity::entity::{decode, ICodedDataTrait};
|
||||
use futures::future::join_all;
|
||||
use serde::Serialize;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
use serde::Deserialize;
|
||||
use crate::db;
|
||||
use crate::providers::perverzija;
|
||||
use crate::providers::Provider;
|
||||
use crate::schema::videos::url;
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||
use crate::util::time::parse_time_to_seconds;
|
||||
use crate::videos::{self, PageInfo, Video_Embed, Video_Item, Videos}; // Make sure Provider trait is imported
|
||||
use crate::videos::ServerOptions;
|
||||
use crate::videos::{self, VideoEmbed, VideoItem};
|
||||
use crate::DbPool;
|
||||
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
HttpRequest(reqwest::Error);
|
||||
HttpRequest(wreq::Error);
|
||||
JsonError(serde_json::Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct PerverzijaDbEntry {
|
||||
url_string: String,
|
||||
tags_strings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PerverzijaProvider {
|
||||
url: String,
|
||||
}
|
||||
@@ -24,119 +44,284 @@ impl PerverzijaProvider {
|
||||
url: "https://tube.perverzija.com/".to_string(),
|
||||
}
|
||||
}
|
||||
async fn get(&self, page: &u8, featured: String) -> Result<Vec<Video_Item>> {
|
||||
async fn get(&self, cache:VideoCache, pool:DbPool, page: u8, featured: String) -> Result<Vec<VideoItem>> {
|
||||
|
||||
let mut prefix_uri = "".to_string();
|
||||
if featured == "featured"{
|
||||
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 mut url_str = format!("{}{}page/{}/", self.url, prefix_uri, page);
|
||||
if page == 1 {
|
||||
url_str = 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?;
|
||||
|
||||
let old_items = match cache.get(&url_str) {
|
||||
Some((time, items)) => {
|
||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||
println!("Cache hit for URL: {}", url_str);
|
||||
return Ok(items.clone());
|
||||
}
|
||||
else{
|
||||
items.clone()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox136)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(url_str.clone()).send().await?;
|
||||
// print!("Response: {:?}\n", response);
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items = self.get_video_items_from_html(text.clone());
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(), pool);
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&url_str);
|
||||
cache.insert(url_str.clone(), video_items.clone());
|
||||
} else{
|
||||
return Ok(old_items);
|
||||
}
|
||||
Ok(video_items)
|
||||
} else {
|
||||
Err("Failed to fetch data".into())
|
||||
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_str.clone(),
|
||||
maxTimeout: 60000,
|
||||
})
|
||||
.await;
|
||||
let video_items = match result {
|
||||
Ok(res) => {
|
||||
// println!("FlareSolverr response: {}", res);
|
||||
self.get_video_items_from_html(res.solution.response, pool)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error solving FlareSolverr: {}", e);
|
||||
return Err("Failed to solve FlareSolverr".into());
|
||||
}
|
||||
};
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&url_str);
|
||||
cache.insert(url_str.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
Ok(video_items)
|
||||
}
|
||||
}
|
||||
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()?;
|
||||
async fn query(&self, cache: VideoCache, pool:DbPool, page: u8, query: &str) -> Result<Vec<VideoItem>> {
|
||||
let mut query_parse = true;
|
||||
let search_string = query.replace(" ", "+");
|
||||
let mut url_str = format!(
|
||||
"{}page/{}/?s={}",
|
||||
self.url, page, search_string
|
||||
);
|
||||
if page == 1 {
|
||||
url_str = format!("{}?s={}", self.url, search_string);
|
||||
}
|
||||
|
||||
if query.starts_with("@studio:") {
|
||||
let studio_name = query.replace("@studio:", "");
|
||||
url_str = format!("{}studio/{}/page/{}/", self.url, studio_name, page);
|
||||
query_parse = false;
|
||||
} else if query.starts_with("@stars:") {
|
||||
let stars_name = query.replace("@stars:", "");
|
||||
url_str = format!("{}stars/{}/page/{}/", self.url, stars_name, page);
|
||||
query_parse = false;
|
||||
}
|
||||
url_str = url_str.replace("page/1/", "");
|
||||
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||
let old_items = match cache.get(&url_str) {
|
||||
Some((time, items)) => {
|
||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||
return Ok(items.clone());
|
||||
}
|
||||
else{
|
||||
let _ = cache.check().await;
|
||||
return Ok(items.clone())
|
||||
}
|
||||
}
|
||||
None => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox136)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(url_str.clone()).send().await?;
|
||||
if response.status().is_success() {
|
||||
let text = response.text().unwrap_or_default();
|
||||
|
||||
println!("{}", &text);
|
||||
Ok(vec![])
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = match query_parse{
|
||||
true => {self.get_video_items_from_html_query(text.clone(), pool).await},
|
||||
false => {self.get_video_items_from_html(text.clone(), pool)}
|
||||
};
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&url_str);
|
||||
cache.insert(url_str.clone(), video_items.clone());
|
||||
} else{
|
||||
return Ok(old_items);
|
||||
}
|
||||
Ok(video_items)
|
||||
} else {
|
||||
Err("Failed to fetch data".into())
|
||||
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_str.clone(),
|
||||
maxTimeout: 60000,
|
||||
})
|
||||
.await;
|
||||
let video_items = match result {
|
||||
Ok(res) => {
|
||||
self.get_video_items_from_html_query(res.solution.response, pool).await
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error solving FlareSolverr: {}", e);
|
||||
return Err("Failed to solve FlareSolverr".into());
|
||||
}
|
||||
};
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&url_str);
|
||||
cache.insert(url_str.clone(), video_items.clone());
|
||||
} else{
|
||||
return Ok(old_items);
|
||||
}
|
||||
Ok(video_items)
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
fn get_video_items_from_html(&self, html: String, pool: DbPool) -> Vec<VideoItem> {
|
||||
if html.is_empty() {
|
||||
println!("HTML is empty");
|
||||
return vec![];
|
||||
}
|
||||
let mut items: Vec<VideoItem> = Vec::new();
|
||||
let video_listing_content = html.split("video-listing-content").collect::<Vec<&str>>()[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 {
|
||||
|
||||
if vid.len() > 20 {
|
||||
continue;
|
||||
}
|
||||
for line in vid.clone(){
|
||||
println!("{}: {}\n\n", index, line);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// for (index, line) in vid.iter().enumerate() {
|
||||
// println!("Line {}: {}", index, line.to_string().trim());
|
||||
// }
|
||||
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]
|
||||
if !vid[1].contains("iframe src="") {
|
||||
continue;
|
||||
}
|
||||
let url_str = 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]
|
||||
.to_string().replace("index.php", "xs1.php");
|
||||
if url_str.starts_with("https://streamtape.com/"){
|
||||
continue; // Skip Streamtape links
|
||||
}
|
||||
let id = url_str.split("data=").collect::<Vec<&str>>()[1]
|
||||
.split("&")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let raw_duration = match vid.len(){
|
||||
let raw_duration = match vid.len() {
|
||||
10 => vid[6].split("time_dur\">").collect::<Vec<&str>>()[1]
|
||||
.split("<")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string(),
|
||||
.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");
|
||||
if !vid[4].contains("srcset=") && vid[4].split("src=\"").collect::<Vec<&str>>().len() == 1{
|
||||
for (index, line) in vid.iter().enumerate(){
|
||||
println!("Line {}: {}\n\n", index, line);
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
let mut thumb = "".to_string();
|
||||
for v in vid.clone(){
|
||||
let line = v.trim();
|
||||
if line.starts_with("<img "){
|
||||
thumb = line.split(" src=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
let embed_html = vid[1].split("data-embed='").collect::<Vec<&str>>()[1]
|
||||
.split("'")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let id_url = vid[1].split("data-url='").collect::<Vec<&str>>()[1]
|
||||
.split("'")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
|
||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||
let _ = db::insert_video(&mut conn, &id_url, &url_str);
|
||||
drop(conn);
|
||||
let referer_url = "https://xtremestream.xyz/".to_string();
|
||||
let embed = VideoEmbed::new(embed_html, url_str.clone());
|
||||
|
||||
let mut tags: Vec<String> = Vec::new(); // Placeholder for tags, adjust as needed
|
||||
|
||||
let studios_parts = vid[7].split("a href=\"").collect::<Vec<&str>>();
|
||||
for studio in studios_parts.iter().skip(1) {
|
||||
if studio.starts_with("https://tube.perverzija.com/studio/"){
|
||||
tags.push(
|
||||
studio.split("/\"").collect::<Vec<&str>>()[0]
|
||||
.replace("https://tube.perverzija.com/studio/", "@studio:")
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for tag in vid[0].split(" ").collect::<Vec<&str>>(){
|
||||
if tag.starts_with("stars-") {
|
||||
let tag_name = tag.split("stars-").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
if !tag_name.is_empty() {
|
||||
tags.push(format!("@stars:{}", tag_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for tag in vid[0].split(" ").collect::<Vec<&str>>(){
|
||||
if tag.starts_with("tag-") {
|
||||
let tag_name = tag.split("tag-").collect::<Vec<&str>>()[1]
|
||||
.to_string();
|
||||
if !tag_name.is_empty() {
|
||||
tags.push(tag_name.replace("-", " ").to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut video_item = VideoItem::new(
|
||||
id,
|
||||
title,
|
||||
embed.source.clone(),
|
||||
"perverzija".to_string(),
|
||||
thumb,
|
||||
duration,
|
||||
).tags(tags);
|
||||
// .embed(embed.clone());
|
||||
let mut format =
|
||||
videos::VideoFormat::new(url_str.clone(), "1080".to_string(), "m3u8".to_string());
|
||||
format.add_http_header("Referer".to_string(), referer_url.clone());
|
||||
if let Some(formats) = video_item.formats.as_mut() {
|
||||
formats.push(format);
|
||||
} else {
|
||||
@@ -147,21 +332,231 @@ impl PerverzijaProvider {
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
async fn get_video_items_from_html_query(&self, html: String, pool:DbPool) -> Vec<VideoItem> {
|
||||
let raw_videos = html
|
||||
.split("video-item post")
|
||||
.collect::<Vec<&str>>()[1..]
|
||||
.to_vec();
|
||||
let futures = raw_videos.into_iter().map(|el| self.get_video_item(el, pool.clone()));
|
||||
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
||||
let items: Vec<VideoItem> = results
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
async fn get_video_item(&self, snippet: &str, pool: DbPool) -> Result<VideoItem> {
|
||||
let vid = snippet.split("\n").collect::<Vec<&str>>();
|
||||
if vid.len() > 30 {
|
||||
return Err("Unexpected video snippet length".into());
|
||||
}
|
||||
|
||||
let mut title = vid[5].split(" title=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||
|
||||
let thumb = match vid[6].split(" src=\"").collect::<Vec<&str>>().len(){
|
||||
1=>{
|
||||
for (index,line) in vid.iter().enumerate() {
|
||||
println!("Line {}: {}", index, line.to_string().trim());
|
||||
}
|
||||
return Err("Failed to parse thumbnail URL".into());
|
||||
}
|
||||
_ => vid[6].split(" src=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string(),
|
||||
};
|
||||
let duration = 0;
|
||||
|
||||
let lookup_url = vid[5].split(" href=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let referer_url = "https://xtremestream.xyz/".to_string();
|
||||
|
||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||
let db_result = db::get_video(&mut conn,lookup_url.clone());
|
||||
match db_result {
|
||||
Ok(Some(entry)) => {
|
||||
if entry.starts_with("{"){ // replace old urls with new json objects
|
||||
let entry = serde_json::from_str::<PerverzijaDbEntry>(entry.as_str())?;
|
||||
let url_str = entry.url_string;
|
||||
let tags = entry.tags_strings;
|
||||
if url_str.starts_with("!"){
|
||||
return Err("Video was removed".into());
|
||||
}
|
||||
let mut id = url_str.split("data=").collect::<Vec<&str>>()[1]
|
||||
.to_string();
|
||||
if id.contains("&"){
|
||||
id = id.split("&").collect::<Vec<&str>>()[0].to_string()
|
||||
}
|
||||
let mut video_item = VideoItem::new(
|
||||
id,
|
||||
title,
|
||||
url_str.clone(),
|
||||
"perverzija".to_string(),
|
||||
thumb,
|
||||
duration,
|
||||
)
|
||||
.tags(tags)
|
||||
;
|
||||
let mut format =
|
||||
videos::VideoFormat::new(url_str.clone(), "1080".to_string(), "m3u8".to_string());
|
||||
format.add_http_header("Referer".to_string(), referer_url.clone());
|
||||
if let Some(formats) = video_item.formats.as_mut() {
|
||||
formats.push(format);
|
||||
} else {
|
||||
video_item.formats = Some(vec![format]);
|
||||
}
|
||||
return Ok(video_item)
|
||||
}
|
||||
else{
|
||||
let _ = db::delete_video(&mut conn,lookup_url.clone());
|
||||
};
|
||||
}
|
||||
Ok(None) => {
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error fetching video from database: {}", e);
|
||||
// return Err(format!("Error fetching video from database: {}", e).into());
|
||||
}
|
||||
}
|
||||
drop(conn);
|
||||
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox136)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(lookup_url.clone()).send().await?;
|
||||
let text = match response.status().is_success(){
|
||||
true => response.text().await?,
|
||||
false => {
|
||||
println!("Failed to fetch video details");
|
||||
return Err("Failed to fetch video details".into());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let mut url_str = text.split("<iframe src=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string().replace("index.php","xs1.php");
|
||||
if !url_str.contains("xtremestream.xyz"){
|
||||
url_str = "!".to_string()
|
||||
}
|
||||
|
||||
let mut tags: Vec<String> = Vec::new(); // Placeholder for tags, adjust as needed
|
||||
|
||||
let studios_parts = text.split("<strong>Studio: </strong>").collect::<Vec<&str>>()[1].split("</div>").collect::<Vec<&str>>()[0].split("<a href=\"").collect::<Vec<&str>>();
|
||||
for studio in studios_parts.iter().skip(1) {
|
||||
if studio.starts_with("https://tube.perverzija.com/studio/"){
|
||||
tags.push(
|
||||
studio.split("/\"").collect::<Vec<&str>>()[0]
|
||||
.replace("https://tube.perverzija.com/studio/", "@studio:")
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if text.contains("<strong>Stars: </strong>"){
|
||||
let stars_parts: Vec<&str> = text.split("<strong>Stars: </strong>").collect::<Vec<&str>>()[1].split("</div>").collect::<Vec<&str>>()[0].split("<a href=\"").collect::<Vec<&str>>();
|
||||
for star in stars_parts.iter().skip(1) {
|
||||
if star.starts_with("https://tube.perverzija.com/stars/"){
|
||||
tags.push(
|
||||
star.split("/\"").collect::<Vec<&str>>()[0]
|
||||
.replace("https://tube.perverzija.com/stars/", "@stars:")
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tags_parts: Vec<&str> = text.split("<strong>Tags: </strong>").collect::<Vec<&str>>()[1].split("</div>").collect::<Vec<&str>>()[0].split("<a href=\"").collect::<Vec<&str>>();
|
||||
for star in tags_parts.iter().skip(1) {
|
||||
if star.starts_with("https://tube.perverzija.com/stars/"){
|
||||
tags.push(
|
||||
star.split("/\"").collect::<Vec<&str>>()[0]
|
||||
.replace("https://tube.perverzija.com/stars/", "@stars:")
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let perverzija_db_entry = PerverzijaDbEntry {
|
||||
url_string: url_str.clone(),
|
||||
tags_strings: tags.clone(),
|
||||
};
|
||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||
let insert_result = db::insert_video(&mut conn, &lookup_url, &serde_json::to_string(&perverzija_db_entry)?);
|
||||
match insert_result{
|
||||
Ok(_) => (),
|
||||
Err(e) => {println!("{:?}", e); }
|
||||
}
|
||||
drop(conn);
|
||||
if !url_str.contains("xtremestream.xyz"){
|
||||
return Err("Video URL does not contain xtremestream.xyz".into());
|
||||
}
|
||||
let mut id = url_str.split("data=").collect::<Vec<&str>>()[1]
|
||||
.to_string();
|
||||
if id.contains("&"){
|
||||
id = id.split("&").collect::<Vec<&str>>()[0].to_string()
|
||||
}
|
||||
// if !vid[6].contains(" src=\""){
|
||||
// for (index,line) in vid.iter().enumerate() {
|
||||
// println!("Line {}: {}", index, line.to_string().trim());
|
||||
// }
|
||||
// }
|
||||
// for (index, line) in vid.iter().enumerate() {
|
||||
// println!("Line {}: {}", index, line.to_string().trim());
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
let mut video_item = VideoItem::new(
|
||||
id,
|
||||
title,
|
||||
url_str.clone(),
|
||||
"perverzija".to_string(),
|
||||
thumb,
|
||||
duration,
|
||||
)
|
||||
.tags(tags);
|
||||
// .embed(embed.clone());
|
||||
let mut format =
|
||||
videos::VideoFormat::new(url_str.clone(), "1080".to_string(), "m3u8".to_string());
|
||||
format.add_http_header("Referer".to_string(), referer_url.clone());
|
||||
if let Some(formats) = video_item.formats.as_mut() {
|
||||
formats.push(format);
|
||||
} else {
|
||||
video_item.formats = Some(vec![format]);
|
||||
}
|
||||
return Ok(video_item);
|
||||
}
|
||||
}
|
||||
|
||||
impl Provider for PerverzijaProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
_channel: String,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
featured: String,
|
||||
) -> Vec<Video_Item> {
|
||||
options: ServerOptions,
|
||||
) -> Vec<VideoItem> {
|
||||
let _ = per_page;
|
||||
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,
|
||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||
Some(q) => self.query(cache, pool, page.parse::<u8>().unwrap_or(1), &q).await,
|
||||
None => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), options.featured.unwrap()).await,
|
||||
};
|
||||
match videos {
|
||||
Ok(v) => v,
|
||||
|
||||
445
src/providers/pmvhaven.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
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::{ServerOptions, 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),
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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, sort: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);
|
||||
request.activeView = sort;
|
||||
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,
|
||||
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).await,
|
||||
None => {
|
||||
self.get(cache, page.parse::<u8>().unwrap_or(1), options.category.unwrap(), sort)
|
||||
.await
|
||||
}
|
||||
};
|
||||
match videos {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Error fetching videos: {}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
360
src/providers/pornhub.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
use crate::schema::videos::url;
|
||||
use crate::util::parse_abbreviated_number;
|
||||
use crate::DbPool;
|
||||
use crate::providers::Provider;
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||
use crate::util::time::parse_time_to_seconds;
|
||||
use crate::videos::{ServerOptions, VideoItem};
|
||||
use error_chain::error_chain;
|
||||
use futures::stream::SplitSink;
|
||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||
use std::env;
|
||||
use std::vec;
|
||||
use wreq::{Client, Proxy};
|
||||
use wreq_util::Emulation;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
HttpRequest(wreq::Error);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PornhubProvider {
|
||||
url: String,
|
||||
}
|
||||
impl PornhubProvider {
|
||||
pub fn new() -> Self {
|
||||
PornhubProvider {
|
||||
url: "https://www.pornhub.com".to_string(),
|
||||
}
|
||||
}
|
||||
async fn get(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
page: u8,
|
||||
sort: &str,
|
||||
) -> Result<Vec<VideoItem>> {
|
||||
let video_url = format!("{}/video?o={}&page={}", self.url, sort, page);
|
||||
let old_items = match cache.get(&video_url) {
|
||||
Some((time, items)) => {
|
||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||
println!("Cache hit for URL: {}", video_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 mut response = client.get(video_url.clone())
|
||||
// .proxy(proxy.clone())
|
||||
.send().await?;
|
||||
if response.status().is_redirection(){
|
||||
|
||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||
// .proxy(proxy)
|
||||
.send().await?;
|
||||
}
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(),"<ul id=\"video");
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
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: video_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,"<ul id=\"video")
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error solving FlareSolverr: {}", e);
|
||||
return Err("Failed to solve FlareSolverr".into());
|
||||
}
|
||||
};
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
Ok(video_items)
|
||||
}
|
||||
}
|
||||
async fn query(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
page: u8,
|
||||
query: &str,
|
||||
sort: &str,
|
||||
) -> Result<Vec<VideoItem>> {
|
||||
let mut split_string = "<ul id=\"video";
|
||||
let search_string = query.to_lowercase().trim().replace(" ", "+");
|
||||
let mut video_url = format!("{}/video/search?search={}&page={}", self.url, search_string, page);
|
||||
if query.starts_with("@"){
|
||||
let url_parts = query[1..].split(":").collect::<Vec<&str>>();
|
||||
video_url = [self.url.to_string(), url_parts[0].to_string(), url_parts[1].replace(" ", "-").to_string(), "videos?page=".to_string()].join("/");
|
||||
video_url += &page.to_string();
|
||||
if query.contains("@model") || query.contains("@pornstar"){
|
||||
split_string = "mostRecentVideosSection";
|
||||
}
|
||||
if query.contains("@channels"){
|
||||
split_string = "<ul class=\"videos row-5-thumbs";
|
||||
}
|
||||
}
|
||||
|
||||
if query.contains("@channels"){
|
||||
video_url += match sort {
|
||||
"mr" => "",
|
||||
"mv" => "&o=vi",
|
||||
"tr" => "&o=ra",
|
||||
_ => "",
|
||||
}
|
||||
} else{
|
||||
video_url += format!("&o={}", sort).as_str();
|
||||
}
|
||||
|
||||
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||
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());
|
||||
} 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 mut response = client.get(video_url.clone())
|
||||
.proxy(proxy.clone())
|
||||
.send().await?;
|
||||
|
||||
if response.status().is_redirection(){
|
||||
|
||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||
.proxy(proxy)
|
||||
.send().await?;
|
||||
}
|
||||
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(),split_string);
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
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: video_url.clone(),
|
||||
maxTimeout: 60000,
|
||||
})
|
||||
.await;
|
||||
let video_items = match result {
|
||||
Ok(res) => self.get_video_items_from_html(res.solution.response,split_string),
|
||||
Err(e) => {
|
||||
println!("Error solving FlareSolverr: {}", e);
|
||||
return Err("Failed to solve FlareSolverr".into());
|
||||
}
|
||||
};
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
Ok(video_items)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_video_items_from_html(&self, html: String, split_string: &str) -> Vec<VideoItem> {
|
||||
if html.is_empty() {
|
||||
println!("HTML is empty");
|
||||
return vec![];
|
||||
}
|
||||
let mut items: Vec<VideoItem> = Vec::new();
|
||||
let video_listing_content = html.split(split_string).collect::<Vec<&str>>()[1].split("Porn in German").collect::<Vec<&str>>()[0];
|
||||
let raw_videos = video_listing_content
|
||||
.split("class=\"pcVideoListItem ")
|
||||
.collect::<Vec<&str>>()[1..]
|
||||
.to_vec();
|
||||
for video_segment in &raw_videos {
|
||||
// let vid = video_segment.split("\n").collect::<Vec<&str>>();
|
||||
// for (index, line) in vid.iter().enumerate() {
|
||||
// println!("Line {}: {}", index, line);
|
||||
// }
|
||||
if video_segment.contains("wrapVideoBlock"){
|
||||
continue; // Skip if the segment is a wrapVideoBlock
|
||||
}
|
||||
let mut video_url: String = String::new();
|
||||
if !video_segment.contains("<a href=\"") {
|
||||
let url_part = video_segment.split("data-video-vkey=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0];
|
||||
video_url = format!("{}{}", self.url, url_part);
|
||||
}
|
||||
else{
|
||||
let url_part = video_segment.split("<a href=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0];
|
||||
if url_part.is_empty() || url_part == "javascript:void(0)" {
|
||||
continue;
|
||||
}
|
||||
video_url = format!("{}{}", self.url, url_part);
|
||||
}
|
||||
if video_url == "https://www.pornhub.comjavascript:void(0)".to_string() {
|
||||
continue;
|
||||
}
|
||||
let mut title = video_segment.split("\" title=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
// html decode
|
||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||
let id = video_segment.split("data-video-id=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let raw_duration = video_segment.split("duration").collect::<Vec<&str>>()[1].split(">").collect::<Vec<&str>>()[1]
|
||||
.split("<")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||
let view_part = match video_segment.split("iews\">").collect::<Vec<&str>>().len(){
|
||||
2 => video_segment.split("iews\">").collect::<Vec<&str>>()[1],
|
||||
3 => video_segment.split("iews\">").collect::<Vec<&str>>()[2],
|
||||
_ => "<var>0<", // Skip if the format is unexpected
|
||||
};
|
||||
let views = parse_abbreviated_number(view_part
|
||||
.split("<var>").collect::<Vec<&str>>()[1]
|
||||
.split("<")
|
||||
.collect::<Vec<&str>>()[0]).unwrap_or(0);
|
||||
|
||||
let thumb = video_segment.split("src=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
|
||||
|
||||
|
||||
let mut uploaderBlock = String::new();
|
||||
let mut uploader_href = vec![];
|
||||
let mut tag = String::new();
|
||||
if video_segment.contains("videoUploaderBlock") {
|
||||
|
||||
uploaderBlock = video_segment.split("videoUploaderBlock").collect::<Vec<&str>>()[1]
|
||||
.to_string();
|
||||
uploader_href = uploaderBlock.split("href=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.split("/").collect::<Vec<&str>>();
|
||||
tag = format!("@{}:{}", uploader_href[1], uploader_href[2].replace("-", " "));
|
||||
|
||||
}
|
||||
|
||||
|
||||
let mut video_item = VideoItem::new(
|
||||
id,
|
||||
title,
|
||||
video_url.to_string(),
|
||||
"pornhub".to_string(),
|
||||
thumb,
|
||||
duration,
|
||||
)
|
||||
;
|
||||
if views > 0 {
|
||||
video_item = video_item.views(views);
|
||||
}
|
||||
if !tag.is_empty() {
|
||||
video_item = video_item.tags(vec![tag])
|
||||
.uploader(uploader_href[2].to_string());
|
||||
}
|
||||
// if video_segment.contains("data-mediabook=\"") {
|
||||
// let preview = video_segment.split("data-mediabook=\"").collect::<Vec<&str>>()[1]
|
||||
// .split("\"")
|
||||
// .collect::<Vec<&str>>()[0]
|
||||
// .to_string();
|
||||
// video_item = video_item.preview(preview);
|
||||
// }
|
||||
|
||||
|
||||
items.push(video_item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
impl Provider for PornhubProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
options: ServerOptions,
|
||||
) -> Vec<VideoItem> {
|
||||
let _ = options;
|
||||
let _ = per_page;
|
||||
let _ = pool; // Ignored in this implementation
|
||||
let mut sort = sort.to_lowercase();
|
||||
if sort.contains("date"){
|
||||
sort = "mr".to_string();
|
||||
}
|
||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||
Some(q) => {
|
||||
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, &sort)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
||||
.await
|
||||
}
|
||||
};
|
||||
match videos {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Error fetching videos: {}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
295
src/providers/redtube.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
use crate::schema::videos::url;
|
||||
use crate::util::parse_abbreviated_number;
|
||||
use crate::DbPool;
|
||||
use crate::providers::Provider;
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||
use crate::util::time::parse_time_to_seconds;
|
||||
use crate::videos::{ServerOptions, VideoItem};
|
||||
use error_chain::error_chain;
|
||||
use futures::stream::SplitSink;
|
||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||
use serde_json::Value;
|
||||
use std::env;
|
||||
use std::os::linux::raw;
|
||||
use std::time::Duration;
|
||||
use std::vec;
|
||||
use wreq::{Client, Proxy};
|
||||
use wreq_util::Emulation;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
HttpRequest(wreq::Error);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RedtubeProvider {
|
||||
url: String,
|
||||
}
|
||||
impl RedtubeProvider {
|
||||
pub fn new() -> Self {
|
||||
RedtubeProvider {
|
||||
url: "https://www.redtube.com".to_string(),
|
||||
}
|
||||
}
|
||||
async fn get(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
page: u8,
|
||||
sort: &str,
|
||||
) -> Result<Vec<VideoItem>> {
|
||||
let video_url = format!("{}/mostviewed", self.url);
|
||||
let old_items = match cache.get(&video_url) {
|
||||
Some((time, items)) => {
|
||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 {
|
||||
println!("Cache hit for URL: {}", video_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 mut response = client.get(video_url.clone())
|
||||
// .proxy(proxy.clone())
|
||||
.send().await?;
|
||||
if response.status().is_redirection(){
|
||||
|
||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||
// .proxy(proxy)
|
||||
.send().await?;
|
||||
}
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
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: video_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(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
Ok(video_items)
|
||||
}
|
||||
}
|
||||
async fn query(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
page: u8,
|
||||
query: &str,
|
||||
sort: &str,
|
||||
) -> Result<Vec<VideoItem>> {
|
||||
let search_string = query.to_lowercase().trim().replace(" ", "+");
|
||||
let video_url = format!("{}/?search={}&page={}", self.url, search_string, page);
|
||||
|
||||
// Check our Video Cache. If the result is younger than 1 hour, we return it.
|
||||
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());
|
||||
} 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 mut response = client.get(video_url.clone())
|
||||
.proxy(proxy.clone())
|
||||
.send().await?;
|
||||
|
||||
if response.status().is_redirection(){
|
||||
|
||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||
.proxy(proxy)
|
||||
.send().await?;
|
||||
}
|
||||
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html_query(text.clone());
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
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: video_url.clone(),
|
||||
maxTimeout: 60000,
|
||||
})
|
||||
.await;
|
||||
let video_items = match result {
|
||||
Ok(res) => self.get_video_items_from_html_query(res.solution.response),
|
||||
Err(e) => {
|
||||
println!("Error solving FlareSolverr: {}", e);
|
||||
return Err("Failed to solve FlareSolverr".into());
|
||||
}
|
||||
};
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&video_url);
|
||||
cache.insert(video_url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
Ok(video_items)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||
if html.is_empty() {
|
||||
println!("HTML is empty");
|
||||
return vec![];
|
||||
}
|
||||
let mut items: Vec<VideoItem> = Vec::new();
|
||||
let video_listing_content = html.split("<script type=\"application/ld+json\">").collect::<Vec<&str>>()[1].split("</script>").collect::<Vec<&str>>()[0];
|
||||
let mut videos: Value = serde_json::from_str(video_listing_content).unwrap();
|
||||
for vid in videos.as_array_mut().unwrap() {
|
||||
let mut video_url: String = vid["embedUrl"].as_str().unwrap_or("").to_string();
|
||||
let mut title: String = vid["name"].as_str().unwrap_or("").to_string();
|
||||
// html decode
|
||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||
let id = video_url.split("=").collect::<Vec<&str>>()[1].to_string();
|
||||
let raw_duration = vid["duration"].as_str().unwrap_or("0");
|
||||
let duration = raw_duration.replace("PT", "").replace("S","").parse::<u32>().unwrap();
|
||||
let views: u64 = vid["interactionCount"].as_u64().unwrap_or(0);
|
||||
let thumb = vid["thumbnailUrl"].as_str().unwrap_or("").to_string();
|
||||
|
||||
let mut video_item = VideoItem::new(
|
||||
id,
|
||||
title,
|
||||
video_url.to_string(),
|
||||
"redtube".to_string(),
|
||||
thumb,
|
||||
duration,
|
||||
)
|
||||
.views(views as u32)
|
||||
;
|
||||
items.push(video_item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
fn get_video_items_from_html_query(&self, html: String) -> Vec<VideoItem> {
|
||||
if html.is_empty() {
|
||||
println!("HTML is empty");
|
||||
return vec![];
|
||||
}
|
||||
let mut items: Vec<VideoItem> = Vec::new();
|
||||
let video_listing_content = html.split("videos_grid").collect::<Vec<&str>>()[1];
|
||||
let videos = video_listing_content.split("<li id=\"tags_videos_").collect::<Vec<&str>>()[1..].to_vec();
|
||||
for vid in videos {
|
||||
// for (i, c) in vid.split("\n").enumerate() {
|
||||
// println!("{}: {}", i, c);
|
||||
// }
|
||||
let id = vid.split("data-video-id=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
||||
let video_url = format!("{}/{}", self.url, id);
|
||||
let title = vid.split(" <a title=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].trim().to_string();
|
||||
let thumb = vid.split("<img").collect::<Vec<&str>>()[1].split(" data-src=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
||||
let raw_duration = vid.split("<span class=\"video-properties tm_video_duration\">").collect::<Vec<&str>>()[1].split("</span>").collect::<Vec<&str>>()[0].trim().to_string();
|
||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||
let views_str = vid.split("<span class='info-views'>").collect::<Vec<&str>>()[1].split("</span>").collect::<Vec<&str>>()[0].trim().to_string();
|
||||
let views = parse_abbreviated_number(&views_str).unwrap_or(0) as u32;
|
||||
let preview = vid.split("<img").collect::<Vec<&str>>()[1].split(" data-mediabook=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
||||
|
||||
let video_item = VideoItem::new(
|
||||
id,
|
||||
title,
|
||||
video_url,
|
||||
"redtube".to_string(),
|
||||
thumb,
|
||||
duration,
|
||||
)
|
||||
.views(views)
|
||||
.preview(preview)
|
||||
;
|
||||
items.push(video_item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Provider for RedtubeProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
options: ServerOptions,
|
||||
) -> Vec<VideoItem> {
|
||||
let _ = options;
|
||||
let _ = per_page;
|
||||
let _ = pool;
|
||||
let mut sort = sort.to_lowercase();
|
||||
if sort.contains("date"){
|
||||
sort = "mr".to_string();
|
||||
}
|
||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||
Some(q) => {
|
||||
self.query(cache, page.parse::<u8>().unwrap_or(1), &q, &sort)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
||||
.await
|
||||
}
|
||||
};
|
||||
match videos {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Error fetching videos: {}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
317
src/providers/rule34video.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
use crate::util::parse_abbreviated_number;
|
||||
use crate::DbPool;
|
||||
use crate::providers::Provider;
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||
use crate::util::time::parse_time_to_seconds;
|
||||
use crate::videos::{ServerOptions, VideoItem};
|
||||
use error_chain::error_chain;
|
||||
use futures::stream::SplitSink;
|
||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||
use std::env;
|
||||
use std::vec;
|
||||
use wreq::{Client, Proxy};
|
||||
use wreq_util::Emulation;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
HttpRequest(wreq::Error);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rule34videoProvider {
|
||||
url: String,
|
||||
}
|
||||
impl Rule34videoProvider {
|
||||
pub fn new() -> Self {
|
||||
Rule34videoProvider {
|
||||
url: "https://rule34video.com".to_string(),
|
||||
}
|
||||
}
|
||||
async fn get(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
page: u8,
|
||||
sort: &str,
|
||||
) -> Result<Vec<VideoItem>> {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
|
||||
let timestamp_millis = now.as_millis(); // u128
|
||||
let expected_sorts = vec!["post_date", "video_viewed", "rating", "duration", "pseudo_random"];
|
||||
let sort = if expected_sorts.contains(&sort) {
|
||||
sort
|
||||
} else {
|
||||
"post_date"
|
||||
};
|
||||
|
||||
let index = format!("rule34video:{}:{}", page, sort);
|
||||
|
||||
let url = format!("{}/?mode=async&function=get_block&block_id=custom_list_videos_most_recent_videos&tag_ids=&sort_by={}&from={}&_={}", self.url, sort, page, timestamp_millis);
|
||||
|
||||
let mut old_items: Vec<VideoItem> = vec![];
|
||||
if !(sort == "pseudo_random") {
|
||||
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 mut response = client.get(url.clone())
|
||||
// .proxy(proxy.clone())
|
||||
.send().await?;
|
||||
while response.status().is_redirection(){
|
||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||
// .proxy(proxy.clone())
|
||||
.send().await?;
|
||||
}
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&url);
|
||||
cache.insert(url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
async fn query(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
page: u8,
|
||||
query: &str,
|
||||
sort: &str,
|
||||
) -> Result<Vec<VideoItem>> {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
let timestamp_millis = now.as_millis(); // u128
|
||||
let expected_sorts = vec!["post_date", "video_viewed", "rating", "duration", "pseudo_random"];
|
||||
let sort = if expected_sorts.contains(&sort) {
|
||||
sort
|
||||
} else {
|
||||
"post_date"
|
||||
};
|
||||
|
||||
let index = format!("rule34video:{}:{}:{}", page, sort, query);
|
||||
|
||||
let url = format!("{}/search/{}/?mode=async&function=get_block&block_id=custom_list_videos_videos_list_search&tag_ids=&sort_by={}&from_videos={}&from_albums={}&_={}", self.url, query, sort, page, page, timestamp_millis);
|
||||
|
||||
// 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 mut response = client.get(url.clone())
|
||||
// .proxy(proxy.clone())
|
||||
.send().await?;
|
||||
|
||||
if response.status().is_redirection(){
|
||||
|
||||
response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap())
|
||||
// .proxy(proxy)
|
||||
.send().await?;
|
||||
}
|
||||
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone());
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&url);
|
||||
cache.insert(url.clone(), video_items.clone());
|
||||
} else {
|
||||
return Ok(old_items);
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
|
||||
if html.is_empty() {
|
||||
println!("HTML is empty");
|
||||
return vec![];
|
||||
}
|
||||
let mut items: Vec<VideoItem> = Vec::new();
|
||||
let video_listing_content = html.split("<div class=\"thumbs clearfix\" id=\"custom_list_videos").collect::<Vec<&str>>()[1].split("<div class=\"pagination\"").collect::<Vec<&str>>()[0].to_string();
|
||||
let raw_videos = video_listing_content
|
||||
.split("<div class=\"item thumb video_")
|
||||
.collect::<Vec<&str>>()[1..]
|
||||
.to_vec();
|
||||
for video_segment in &raw_videos {
|
||||
// let vid = video_segment.split("\n").collect::<Vec<&str>>()[1]
|
||||
// for (index, line) in vid.iter().enumerate() {
|
||||
// println!("Line {}: {}", index, line);
|
||||
// }
|
||||
|
||||
if video_segment.contains("https://rule34video.com/images/advertisements"){
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut title = video_segment.split("<div class=\"thumb_title\">").collect::<Vec<&str>>()[1]
|
||||
.split("<")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
// html decode
|
||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||
let id = video_segment.split("https://rule34video.com/video/").collect::<Vec<&str>>()[1].split("/").collect::<Vec<&str>>()[0].to_string();
|
||||
let raw_duration = video_segment.split("<div class=\"time\">").collect::<Vec<&str>>()[1]
|
||||
.split("<")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
|
||||
let views = parse_abbreviated_number(&video_segment
|
||||
.split("<div class=\"views\">").collect::<Vec<&str>>()[1].split("</svg>").collect::<Vec<&str>>()[1]
|
||||
.split("<")
|
||||
.collect::<Vec<&str>>()[0]).unwrap_or(0);
|
||||
//https://rule34video.com/get_file/47/5e71602b7642f9b997f90c979a368c99b8aad90d89/3942000/3942353/3942353_preview.mp4/
|
||||
//https://rule34video.com/get_file/47/5e71602b7642f9b997f90c979a368c99b8aad90d89/3942000/3942353/3942353_preview.mp4/
|
||||
let thumb = video_segment.split("<img class=\"thumb lazy-load\" src=\"").collect::<Vec<&str>>()[1].split("data-original=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let url = video_segment.split("<a class=\"th js-open-popup\" href=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
let preview = video_segment.split("<div class=\"img wrap_image\" data-preview=\"").collect::<Vec<&str>>()[1]
|
||||
.split("\"")
|
||||
.collect::<Vec<&str>>()[0]
|
||||
.to_string();
|
||||
|
||||
|
||||
let mut video_item = VideoItem::new(
|
||||
id,
|
||||
title,
|
||||
url.to_string(),
|
||||
"Rule34video".to_string(),
|
||||
thumb,
|
||||
duration,
|
||||
)
|
||||
.views(views)
|
||||
// .preview(preview)
|
||||
;
|
||||
|
||||
|
||||
items.push(video_item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
impl Provider for Rule34videoProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
options: ServerOptions,
|
||||
) -> Vec<VideoItem> {
|
||||
let _ = options;
|
||||
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, &sort)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
self.get(cache, page.parse::<u8>().unwrap_or(1), &sort)
|
||||
.await
|
||||
}
|
||||
};
|
||||
match videos {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Error fetching videos: {}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
381
src/providers/spankbang.rs
Normal file
@@ -0,0 +1,381 @@
|
||||
use std::vec;
|
||||
use std::env;
|
||||
use error_chain::error_chain;
|
||||
use futures::future::join_all;
|
||||
use htmlentity::entity::{decode, ICodedDataTrait};
|
||||
use crate::db;
|
||||
use crate::providers::Provider;
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr};
|
||||
use crate::videos::ServerOptions;
|
||||
use crate::videos::{VideoItem};
|
||||
use crate::DbPool;
|
||||
use std::collections::HashMap;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
HttpRequest(wreq::Error);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SpankbangProvider {
|
||||
url: String,
|
||||
}
|
||||
impl SpankbangProvider {
|
||||
pub fn new() -> Self {
|
||||
SpankbangProvider {
|
||||
url: "https://spankbang.com/".to_string()
|
||||
}
|
||||
}
|
||||
async fn get(&self, cache:VideoCache, pool: DbPool, page: u8, sort: String) -> Result<Vec<VideoItem>> {
|
||||
|
||||
let url = format!("{}{}/{}/", self.url, sort, page);
|
||||
|
||||
let old_items = match cache.get(&url) {
|
||||
Some((time, items)) => {
|
||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||
println!("Cache hit for URL: {}", url);
|
||||
return Ok(items.clone());
|
||||
}
|
||||
else{
|
||||
items.clone()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox136)
|
||||
.cert_verification(false)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(url.clone()).send().await?;
|
||||
let mut cookies_string = String::new();
|
||||
if let Some(_) = response.headers().get_all("set-cookie").iter().next() {
|
||||
for _ in response.headers().get_all("set-cookie").iter() {
|
||||
let mut cookies_map = HashMap::new();
|
||||
for value in response.headers().get_all("set-cookie").iter() {
|
||||
if let Ok(cookie_str) = value.to_str() {
|
||||
if let Some((k, v)) = cookie_str.split_once('=') {
|
||||
let key = k.trim();
|
||||
let val = v.split(';').next().unwrap_or("").trim();
|
||||
cookies_map.insert(key.to_string(), val.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
cookies_string = cookies_map
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}={}", k, v))
|
||||
.collect::<Vec<String>>()
|
||||
.join("; ");
|
||||
}
|
||||
}
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(), &client, cookies_string, pool.clone()).await;
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&url);
|
||||
cache.insert(url.clone(), video_items.clone());
|
||||
} else{
|
||||
return Ok(old_items);
|
||||
}
|
||||
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, &client,String::new(), pool.clone()).await
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
async fn query(&self, cache: VideoCache, pool: DbPool, page: u8, query: &str) -> Result<Vec<VideoItem>> {
|
||||
let url = format!("{}s/{}/{}/", self.url, query.replace(" ", "+"), page);
|
||||
|
||||
let old_items = match cache.get(&url) {
|
||||
Some((time, items)) => {
|
||||
if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 {
|
||||
println!("Cache hit for URL: {}", url);
|
||||
return Ok(items.clone());
|
||||
}
|
||||
else{
|
||||
items.clone()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox136)
|
||||
.cert_verification(false)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(url.clone()).send().await?;
|
||||
let mut cookies_string = String::new();
|
||||
if let Some(_) = response.headers().get_all("set-cookie").iter().next() {
|
||||
for _ in response.headers().get_all("set-cookie").iter() {
|
||||
let mut cookies_map = HashMap::new();
|
||||
for value in response.headers().get_all("set-cookie").iter() {
|
||||
if let Ok(cookie_str) = value.to_str() {
|
||||
if let Some((k, v)) = cookie_str.split_once('=') {
|
||||
let key = k.trim();
|
||||
let val = v.split(';').next().unwrap_or("").trim();
|
||||
cookies_map.insert(key.to_string(), val.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
cookies_string = cookies_map
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}={}", k, v))
|
||||
.collect::<Vec<String>>()
|
||||
.join("; ");
|
||||
}
|
||||
}
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let video_items: Vec<VideoItem> = self.get_video_items_from_html(text.clone(), &client, cookies_string, pool.clone()).await;
|
||||
if !video_items.is_empty() {
|
||||
cache.remove(&url);
|
||||
cache.insert(url.clone(), video_items.clone());
|
||||
} else{
|
||||
return Ok(old_items);
|
||||
}
|
||||
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, &client, String::new(), pool.clone()).await
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_video_url(&self, url:String, client:&Client, cookies: String, pool: DbPool) -> Result<String> {
|
||||
|
||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||
let db_result = db::get_video(&mut conn,url.clone());
|
||||
drop(conn);
|
||||
match db_result {
|
||||
Ok(Some(video_url)) => {
|
||||
return Ok(video_url);
|
||||
}
|
||||
Ok(None) => (),
|
||||
Err(e) => {
|
||||
println!("Error fetching video from database: {}", e);
|
||||
// return Err(format!("Error fetching video from database: {}", e).into());
|
||||
}
|
||||
}
|
||||
let response = client.get(url.clone()).header("Cookie", cookies.clone()).send().await?;
|
||||
|
||||
let mut response = response;
|
||||
while response.status().as_u16() == 429 {
|
||||
// println!("Received 429 Too Many Requests. Waiting 10 seconds before retrying...");
|
||||
ntex::time::sleep(ntex::time::Seconds(60)).await;
|
||||
response = client.get(url.clone()).header("Cookie", cookies.clone()).send().await?;
|
||||
}
|
||||
|
||||
if response.status().is_success() {
|
||||
let text = response.text().await?;
|
||||
let lines = text.split("\n").collect::<Vec<&str>>();
|
||||
let url_line = lines.iter()
|
||||
.find(|s| s.trim_start().starts_with("<source src=") && s.contains("type=\"video/mp4\""))
|
||||
.unwrap_or(&"");
|
||||
let new_url = url_line.split("src=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
||||
let mut conn = pool.get().expect("couldn't get db connection from pool");
|
||||
let _ = db::insert_video(&mut conn, &url, &new_url);
|
||||
drop(conn);
|
||||
return Ok(new_url)
|
||||
}
|
||||
Err(Error::from("Failed to get video URL"))
|
||||
}
|
||||
|
||||
async fn parse_video_item(
|
||||
&self,
|
||||
mut html: String,
|
||||
client: &Client,
|
||||
cookies: String,
|
||||
pool: DbPool
|
||||
) -> Result<VideoItem> {
|
||||
if html.contains("<!-- Video list block -->") {
|
||||
html = html.split("<!-- Video list block -->").collect::<Vec<&str>>()[0].to_string();
|
||||
}
|
||||
|
||||
let vid = html.split("\n").collect::<Vec<&str>>();
|
||||
if vid.len() > 200 {
|
||||
return Err("Video item has too many lines".into());
|
||||
}
|
||||
// for (index ,line) in vid.iter().enumerate() {
|
||||
// println!("Line {}: {}", index, line);
|
||||
// }
|
||||
let title_line = vid.iter()
|
||||
.find(|s| s.trim_start().starts_with("<a href=") && s.contains("title="))
|
||||
.unwrap_or(&"");
|
||||
let mut title = title_line.split("title=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
||||
title = decode(title.as_bytes()).to_string().unwrap_or(title);
|
||||
|
||||
let thumb_line = vid.iter()
|
||||
.find(|s| s.trim_start().starts_with("data-src=") && s.contains(".jpg\""))
|
||||
.unwrap_or(&"");
|
||||
let thumb = thumb_line.split("data-src=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
||||
// let preview_line = vid.iter()
|
||||
// .find(|s: &&&str| s.trim_start().starts_with("<source data-src=") && s.contains("mp4"))
|
||||
// .unwrap_or(&"");
|
||||
// let mut preview = "".to_string();
|
||||
// if vid[15].contains("data-preview=\""){
|
||||
// preview = vid[15].split("data-preview=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
||||
// }
|
||||
// else{
|
||||
// preview = preview_line.split("data-src=\"").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0].to_string();
|
||||
// }
|
||||
let duration_str = vid[64].split("m").collect::<Vec<&str>>()[0];
|
||||
let duration: u32 = duration_str.parse::<u32>().unwrap_or(0) * 60;
|
||||
// let view_and_rating_str: Vec<&str> = vid.iter().copied().filter(|s| s.contains("<span class=\"md:text-body-md\">")).collect();
|
||||
// let views_str = view_and_rating_str[0].split(">").collect::<Vec<&str>>()[1].split("K<").collect::<Vec<&str>>()[0];
|
||||
// let views = (views_str.parse::<f32>().unwrap_or(0.0) * 1000.0) as u32;
|
||||
// let rate_str = view_and_rating_str[1].split(">").collect::<Vec<&str>>()[1].split("%<").collect::<Vec<&str>>()[0];
|
||||
// let rating = rate_str.parse::<f32>().unwrap_or(0.0);
|
||||
let url_part = vid.iter().find(|s| s.contains("<a href=\"/")).unwrap().split("<a href=\"/").collect::<Vec<&str>>()[1].split("\"").collect::<Vec<&str>>()[0];
|
||||
let url = match self.get_video_url(self.url.clone() + url_part, client, cookies, pool).await {
|
||||
Ok(video_url) => video_url,
|
||||
Err(e) => {
|
||||
print!("Error fetching video URL: {}", e);
|
||||
return Err("Failed to get video URL".into());
|
||||
}
|
||||
};
|
||||
let id = url_part.split("/").collect::<Vec<&str>>()[0].to_string();
|
||||
|
||||
// let quality_str = match vid[25].contains("<"){
|
||||
// true => vid[25].split(">").collect::<Vec<&str>>()[1].split("<").collect::<Vec<&str>>()[0],
|
||||
// false => "SD",
|
||||
// };
|
||||
// let quality = match quality_str{
|
||||
// "HD" => "1080",
|
||||
// "4k" => "2160",
|
||||
// "SD" => "720",
|
||||
// _ => "1080",
|
||||
// };
|
||||
|
||||
let video_item = VideoItem::new(id, title, url.clone().to_string(), "spankbang".to_string(), thumb, duration)
|
||||
// .views(views)
|
||||
// .rating(rating)
|
||||
// .formats(vec![format])
|
||||
// .preview(preview)
|
||||
;
|
||||
Ok(video_item)
|
||||
}
|
||||
|
||||
async fn get_video_items_from_html(&self, html: String, client: &Client, cookies:String, pool: DbPool) -> Vec<VideoItem> {
|
||||
if html.is_empty() {
|
||||
println!("HTML is empty");
|
||||
return vec![];
|
||||
}
|
||||
let items: Vec<VideoItem> = Vec::new();
|
||||
let split_html = html.split("\"video-list").collect::<Vec<&str>>();
|
||||
if split_html.len() < 2 {
|
||||
println!("Could not find video-list in HTML");
|
||||
return items;
|
||||
}
|
||||
let video_listing_content = format!("{}{}", split_html[1], split_html.get(2).unwrap_or(&""));
|
||||
let raw_videos_vec = video_listing_content
|
||||
.split("data-testid=\"video-item\"")
|
||||
.collect::<Vec<&str>>();
|
||||
if raw_videos_vec.len() < 2 {
|
||||
println!("Could not find video-item in HTML");
|
||||
return items;
|
||||
}
|
||||
let raw_videos = raw_videos_vec[1..].to_vec();
|
||||
println!("Found {} video items", raw_videos.len());
|
||||
let futures = raw_videos.into_iter().map(|el| self.parse_video_item(el.to_string(), client, cookies.clone(), pool.clone()));
|
||||
let results: Vec<Result<VideoItem>> = join_all(futures).await;
|
||||
let video_items: Vec<VideoItem> = results
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
return video_items;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Provider for SpankbangProvider {
|
||||
async fn get_videos(
|
||||
&self,
|
||||
cache: VideoCache,
|
||||
pool: DbPool,
|
||||
mut sort: String,
|
||||
query: Option<String>,
|
||||
page: String,
|
||||
per_page: String,
|
||||
options: ServerOptions,
|
||||
) -> Vec<VideoItem> {
|
||||
let _ = options;
|
||||
let _ = per_page;
|
||||
let _ = pool;
|
||||
|
||||
if sort == "date"{
|
||||
sort = "trending_videos".to_string();
|
||||
}
|
||||
let videos: std::result::Result<Vec<VideoItem>, Error> = match query {
|
||||
Some(q) => self.query(cache, pool, page.parse::<u8>().unwrap_or(1), &q).await,
|
||||
None => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), sort).await,
|
||||
};
|
||||
match videos {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!("Error fetching videos: {}", e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/schema.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
videos (id) {
|
||||
id -> Text,
|
||||
url -> Text,
|
||||
}
|
||||
}
|
||||
@@ -16,23 +16,23 @@ pub struct Channel {
|
||||
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 options: Vec<ChannelOption>,
|
||||
pub nsfw: bool, //true
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Channel_Option {
|
||||
pub struct ChannelOption {
|
||||
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 options: Vec<FilterOption>, //[],
|
||||
pub multiSelect: bool, //true
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Filter_Option{
|
||||
pub struct FilterOption{
|
||||
pub id: String, //"sort",
|
||||
pub title: String, //"Sort",
|
||||
}
|
||||
@@ -44,11 +44,11 @@ pub struct Options {
|
||||
pub description: String, //"Sort the videos by new or old.",
|
||||
pub systemImage: String, //"sort.image",
|
||||
pub colorName: String, //"blue",
|
||||
pub options: Vec<Option_Value>,
|
||||
pub options: Vec<OptionValue>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Option_Value {
|
||||
pub struct OptionValue {
|
||||
pub id: String, //"new",
|
||||
pub title: String, //"New",
|
||||
pub description: Option<String>, //"Sort the videos by new or old."
|
||||
@@ -107,15 +107,19 @@ impl Status {
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn add_notice(&mut self, notice: Notice) {
|
||||
self.notices.push(notice);
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn add_channel(&mut self, channel: Channel) {
|
||||
self.channels.push(channel);
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn add_option(&mut self, option: Options) {
|
||||
self.options.push(option);
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn add_category(&mut self, category: String) {
|
||||
self.categories.push(category);
|
||||
}
|
||||
|
||||
60
src/util/cache.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::time::{SystemTime};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
use crate::videos::VideoItem;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VideoCache{
|
||||
cache: Arc<Mutex<std::collections::HashMap<String, (SystemTime, Vec<VideoItem>)>>>, // url -> time+Items
|
||||
}
|
||||
impl VideoCache {
|
||||
pub fn new() -> Self {
|
||||
VideoCache {
|
||||
cache: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<(SystemTime, Vec<VideoItem>)> {
|
||||
let cache = self.cache.lock().ok()?;
|
||||
cache.get(key).cloned()
|
||||
}
|
||||
|
||||
pub fn insert(&self, key: String, value: Vec<VideoItem>) {
|
||||
if let Ok(mut cache) = self.cache.lock() {
|
||||
cache.insert(key.clone(), (SystemTime::now(), value.clone()));
|
||||
}
|
||||
}
|
||||
pub fn remove(&self, key: &str) {
|
||||
if let Ok(mut cache) = self.cache.lock() {
|
||||
cache.remove(key);
|
||||
}
|
||||
}
|
||||
pub fn entries(&self) -> Option<Vec<(String, (SystemTime, Vec<VideoItem>))>> {
|
||||
if let Ok(cache) = self.cache.lock() {
|
||||
// Return a cloned vector of the cache entries
|
||||
return Some(cache.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn check(&self) -> Result<(), Box<dyn std::error::Error>>{
|
||||
let iter = match self.entries(){
|
||||
Some(iter) => iter,
|
||||
None => return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Could not get entries")))
|
||||
};
|
||||
|
||||
for (key, (time, _items)) in iter {
|
||||
if let Ok(elapsed) = time.elapsed() {
|
||||
if elapsed > Duration::from_secs(60*60){
|
||||
println!("Key: {}, elapsed: {:?}", key, elapsed);
|
||||
self.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
98
src/util/flaresolverr.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::json;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct FlareSolverrRequest {
|
||||
pub cmd: String,
|
||||
pub url: String,
|
||||
pub maxTimeout: u32,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct FlaresolverrCookie {
|
||||
name: String, //"cf_clearance",
|
||||
value: String, //"lnKoXclrIp_mDrWJFfktPGm8GDyxjSpzy9dx0qDTiRg-1748689259-1.2.1.1-AIFERAPCdCSvvdu1mposNdUpKV9wHZXBpSI2L9k9TaKkPcqmomON_XEb6ZtRBtrmQu_DC8AzKllRg2vNzVKOUsvv9ndjQ.vv8Z7cNkgzpIbGFy96kXyAYH2mUk3Q7enZovDlEbK5kpV3Sbmd2M3_bUCBE1WjAMMdXlyNElH1LOpUm149O9hrluXjAffo4SwHI4HO0UckBPWBlBqhznKPgXxU0g8VHLDeYnQKViY8rP2ud4tyzKnJUxuYXzr4aWBNMp6TESp49vesRiel_Y5m.rlTY4zSb517S9iPbEQiYHRI.uH5mMHVI3jvJl0Mx94tPrpFnkhDdmzL3DRSllJe9k786Lf21I9WBoH2cCR3yHw",
|
||||
domain: String, //".discord.com",
|
||||
path: String, //"/",
|
||||
expires: f64, //1780225259.237105,
|
||||
size: u64, //438,
|
||||
httpOnly: bool, //true,
|
||||
secure: bool, //true,
|
||||
session: bool, //false,
|
||||
sameSite: Option<String>, //"None",
|
||||
priority: String, //"Medium",
|
||||
sameParty: bool, //false,
|
||||
sourceScheme: String, //"Secure",
|
||||
sourcePort: u32, //443,
|
||||
partitionKey: Option<String>, //"https://perverzija.com"
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct FlareSolverrSolution {
|
||||
url: String,
|
||||
status: u32,
|
||||
pub response: String,
|
||||
headers: HashMap<String, String>,
|
||||
cookies: Vec<FlaresolverrCookie>,
|
||||
userAgent: String,
|
||||
}
|
||||
// impl FlareSolverrSolution {
|
||||
// fn to_client(&self,){
|
||||
// let mut headers = header::HeaderMap::new();
|
||||
// for (h, v) in &self.headers {
|
||||
// println!("{}: {}", h, v);
|
||||
// headers.insert(
|
||||
// header::HeaderName::from_bytes(h.as_bytes()).unwrap(),
|
||||
// header::HeaderValue::from_str(v).unwrap(),
|
||||
// );
|
||||
// }
|
||||
// // let client = reqwest::Client::builder()
|
||||
// // .danger_accept_invalid_certs(true)
|
||||
// // .
|
||||
// // .build().unwrap();
|
||||
// }
|
||||
// }
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct FlareSolverrResponse {
|
||||
status: String,
|
||||
message: String,
|
||||
pub solution: FlareSolverrSolution,
|
||||
startTimestamp: u64,
|
||||
endTimestamp: u64,
|
||||
version: String,
|
||||
}
|
||||
pub struct Flaresolverr {
|
||||
url: String
|
||||
}
|
||||
impl Flaresolverr {
|
||||
pub fn new(url: String) -> Self {
|
||||
Flaresolverr {
|
||||
url: url
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn solve(
|
||||
&self,
|
||||
request: FlareSolverrRequest,
|
||||
) -> Result<FlareSolverrResponse, Box<dyn std::error::Error>> {
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox136)
|
||||
.build()?;
|
||||
|
||||
let response = client
|
||||
.post(&self.url)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&json!({
|
||||
"cmd": request.cmd,
|
||||
"url": request.url,
|
||||
"maxTimeout": request.maxTimeout,
|
||||
}))
|
||||
.send().await?;
|
||||
|
||||
let body: FlareSolverrResponse = response.json::<FlareSolverrResponse>().await?;
|
||||
Ok(body)
|
||||
}
|
||||
}
|
||||
@@ -1 +1,43 @@
|
||||
pub mod time;
|
||||
pub mod flaresolverr;
|
||||
pub mod cache;
|
||||
|
||||
pub fn parse_abbreviated_number(s: &str) -> Option<u32> {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (num_part, suffix) = s
|
||||
.chars()
|
||||
.partition::<String, _>(|c| c.is_ascii_digit() || *c == '.');
|
||||
let multiplier = match suffix.trim().to_ascii_uppercase().as_str() {
|
||||
"K" => 1_000.0,
|
||||
"M" => 1_000_000.0,
|
||||
"B" => 1_000_000_000.0,
|
||||
"" => 1.0,
|
||||
_ => return None,
|
||||
};
|
||||
num_part.parse::<f64>().ok().map(|n| (n * multiplier) as u32)
|
||||
}
|
||||
|
||||
pub fn interleave<T: Clone>(lists: &[Vec<T>]) -> Vec<T> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
if lists.is_empty() {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Find the maximum length among the lists
|
||||
let max_len = lists.iter().map(|l| l.len()).max().unwrap_or(0);
|
||||
|
||||
// Interleave elements
|
||||
for i in 0..max_len {
|
||||
for list in lists {
|
||||
if let Some(item) = list.get(i) {
|
||||
result.push(item.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
125
src/videos.rs
@@ -1,7 +1,23 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct Videos_Request {
|
||||
pub struct VideosRequest {
|
||||
//"versionInstallDate":"2025-06-03T18:20:20Z","languageCode":"en","appInstallDate":"2025-06-03T18:20:20Z","server":"spacemoehre","sexu
|
||||
pub clientHash: Option<String>, // "a07b23c9b07813c65050e2a4041ca777",
|
||||
pub blockedKeywords: Option<String>, // "kittens",
|
||||
pub countryCode: Option<String>, // "DE",
|
||||
pub clientVersion: Option<String>, // "2.1.4-22b",
|
||||
pub timestamp: Option<String>, // "1748976686",
|
||||
pub blockedUploaders: Option<String>, // "",
|
||||
pub anonId: Option<String>, // "1AB8A060-A47D-47EF-B9CB-63980ED84C8A",
|
||||
pub debugTools: Option<bool>, // false,
|
||||
pub versionInstallDate: Option<String>, // "2025-06-03T18:20:20Z",
|
||||
pub languageCode: Option<String>, // "en",
|
||||
pub appInstallDate: Option<String>, // "2025-06-03T18:20:20Z",
|
||||
pub server: Option<String>, // "spacemoehre",
|
||||
pub sexuality: Option<String>, // "straight",
|
||||
pub channel: Option<String>, //"youtube",
|
||||
pub sort: Option<String>, //"new",
|
||||
pub query: Option<String>, //"kittens",
|
||||
@@ -10,7 +26,17 @@ pub struct Videos_Request {
|
||||
// Your server's global options will be sent in the videos request
|
||||
// pub flavor: "mint chocolate chip"
|
||||
pub featured: Option<String>, // "featured",
|
||||
pub category: Option<String>, // "pmv"
|
||||
pub sites: Option<String>, //
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct ServerOptions{
|
||||
pub featured: Option<String>, // "featured",
|
||||
pub category: Option<String>, // "pmv"
|
||||
pub sites: Option<String>, //
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct PageInfo {
|
||||
pub hasNextPage: bool, // true,
|
||||
@@ -19,20 +45,20 @@ pub struct PageInfo {
|
||||
|
||||
|
||||
#[derive(serde::Serialize, Debug, Clone)]
|
||||
pub struct Video_Embed{
|
||||
html: String,
|
||||
source: String,
|
||||
pub struct VideoEmbed{
|
||||
pub html: String,
|
||||
pub source: String,
|
||||
}
|
||||
impl Video_Embed {
|
||||
impl VideoEmbed {
|
||||
pub fn new(html: String, source: String) -> Self {
|
||||
Video_Embed {
|
||||
VideoEmbed {
|
||||
html,
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(serde::Serialize, Debug, Clone)]
|
||||
pub struct Video_Item {
|
||||
pub struct VideoItem {
|
||||
pub duration: u32, // 110,
|
||||
pub views: Option<u32>, // 14622653,
|
||||
pub rating: Option<f32>, // 0.0,
|
||||
@@ -46,10 +72,12 @@ pub struct Video_Item {
|
||||
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
|
||||
pub formats: Option<Vec<VideoFormat>>, // Additional HTTP headers if needed
|
||||
pub embed: Option<VideoEmbed>, // Optional embed information
|
||||
pub preview: Option<String>
|
||||
}
|
||||
impl Video_Item {
|
||||
#[allow(dead_code)]
|
||||
impl VideoItem {
|
||||
pub fn new(
|
||||
id: String,
|
||||
title: String,
|
||||
@@ -58,7 +86,7 @@ impl Video_Item {
|
||||
thumb: String,
|
||||
duration: u32,
|
||||
) -> Self {
|
||||
Video_Item {
|
||||
VideoItem {
|
||||
duration: duration, // Placeholder, adjust as needed
|
||||
views: None, // Placeholder, adjust as needed
|
||||
rating: None, // Placeholder, adjust as needed
|
||||
@@ -74,12 +102,60 @@ impl Video_Item {
|
||||
uploadedAt: None,
|
||||
formats: None, // Placeholder for formats
|
||||
embed: None, // Placeholder for embed information
|
||||
preview: None
|
||||
}
|
||||
}
|
||||
pub fn tags(mut self, tags: Vec<String>) -> Self {
|
||||
self.tags = Some(tags);
|
||||
self
|
||||
}
|
||||
pub fn uploader(mut self, uploader: String) -> Self {
|
||||
self.uploader = Some(uploader);
|
||||
self
|
||||
}
|
||||
pub fn uploader_url(mut self, uploader_url: String) -> Self {
|
||||
self.uploaderUrl = Some(uploader_url);
|
||||
self
|
||||
}
|
||||
pub fn verified(mut self, verified: bool) -> Self {
|
||||
self.verified = Some(verified);
|
||||
self
|
||||
}
|
||||
pub fn views(mut self, views: u32) -> Self {
|
||||
self.views = Some(views);
|
||||
self
|
||||
}
|
||||
pub fn rating(mut self, rating: f32) -> Self {
|
||||
self.rating = Some(rating);
|
||||
self
|
||||
}
|
||||
pub fn uploaded_at(mut self, uploaded_at: u64) -> Self {
|
||||
self.uploadedAt = Some(uploaded_at);
|
||||
self
|
||||
}
|
||||
pub fn formats(mut self, formats: Vec<VideoFormat>) -> Self {
|
||||
self.formats = Some(formats);
|
||||
self
|
||||
}
|
||||
pub fn add_format(mut self, format: VideoFormat){
|
||||
if let Some(formats) = self.formats.as_mut() {
|
||||
formats.push(format);
|
||||
} else {
|
||||
self.formats = Some(vec![format]);
|
||||
}
|
||||
}
|
||||
pub fn embed(mut self, embed: VideoEmbed) -> Self {
|
||||
self.embed = Some(embed);
|
||||
self
|
||||
}
|
||||
pub fn preview(mut self, preview: String) -> Self {
|
||||
self.preview = Some(preview);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, Clone)]
|
||||
pub struct Video_Format {
|
||||
pub struct VideoFormat {
|
||||
url: String,
|
||||
quality: String,
|
||||
format: String,
|
||||
@@ -105,14 +181,16 @@ pub struct Video_Format {
|
||||
video_ext: Option<String>,
|
||||
resolution: Option<String>,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
|
||||
}
|
||||
impl Video_Format {
|
||||
impl VideoFormat {
|
||||
pub fn new(url: String, quality: String, format: String) -> Self {
|
||||
Video_Format {
|
||||
let _ = format;
|
||||
VideoFormat {
|
||||
url,
|
||||
quality,
|
||||
format,
|
||||
format_id: None,
|
||||
format: "mp4".to_string(), // Default format
|
||||
format_id: Some("mp4-1080".to_string()),
|
||||
format_note: None,
|
||||
filesize: None,
|
||||
asr: None,
|
||||
@@ -122,16 +200,16 @@ impl Video_Format {
|
||||
tbr: None,
|
||||
language: None,
|
||||
language_preference: None,
|
||||
ext: None,
|
||||
ext: Some("mp4".to_string()),
|
||||
vcodec: None,
|
||||
acodec: None,
|
||||
dynamic_range: None,
|
||||
abr: None,
|
||||
vbr: None,
|
||||
container: None,
|
||||
protocol: None,
|
||||
audio_ext: None,
|
||||
video_ext: None,
|
||||
protocol: Some("m3u8_native".to_string()),
|
||||
audio_ext: Some("none".to_string()),
|
||||
video_ext: Some("mp4".to_string()),
|
||||
resolution: None,
|
||||
http_headers: None,
|
||||
}
|
||||
@@ -144,9 +222,14 @@ impl Video_Format {
|
||||
headers.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn protocol(mut self, protocol: String) -> Self {
|
||||
self.protocol = Some(protocol);
|
||||
self
|
||||
}
|
||||
}
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct Videos {
|
||||
pub pageInfo: PageInfo,
|
||||
pub items: Vec<Video_Item>,
|
||||
pub items: Vec<VideoItem>,
|
||||
}
|
||||
|
||||
2
supervisord/burpsuite.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
/headless/.venv/bin/python3 /app/burp/start_burp.py
|
||||
1
supervisord/hottub.sh
Normal file
@@ -0,0 +1 @@
|
||||
/app/target/release/hottub
|
||||
32
supervisord/supervisord.conf
Normal file
@@ -0,0 +1,32 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/null
|
||||
loglevel=error
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile_maxbytes = 0
|
||||
|
||||
[program:hottub]
|
||||
command=bash /app/supervisord/hottub.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stderr_logfile=/dev/stderr
|
||||
directory=/app
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile_maxbytes = 0
|
||||
|
||||
[program:vnc]
|
||||
command=/dockerstartup/vnc_startup.sh --wait
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile_maxbytes = 0
|
||||
|
||||
[program:burpsuite]
|
||||
command=bash /app/supervisord/burpsuite.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stderr_logfile=/dev/stderr
|
||||
stdout_logfile_maxbytes = 0
|
||||
stderr_logfile_maxbytes = 0
|
||||