This commit is contained in:
Simon
2026-05-20 14:28:11 +00:00
committed by ForgeCode
parent 2a72e08d8a
commit 2ec9137df9
6 changed files with 792 additions and 0 deletions

View File

@@ -306,6 +306,11 @@ const PROVIDERS: &[ProviderDef] = &[
module: "allpornstream",
ty: "AllPornStreamProvider",
},
ProviderDef {
id: "tube8",
module: "tube8",
ty: "Tube8Provider",
},
];
fn main() {

View File

@@ -63,6 +63,7 @@ This is the current implementation inventory as of this snapshot of the repo. Us
| `yesporn` | `mainstream-tube` | no | no | Preview format examples. |
| `youjizz` | `mainstream-tube` | no | no | Mainstream tube provider. |
| `youporn` | `mainstream-tube` | no | no | Pornhub-network HTML provider with watch-page playback URLs and tag/channel/pornstar shortcuts. |
| `tube8` | `mainstream-tube` | no | yes | Aylo/MindGeek platform scraper; redirect proxy fetches signed `/media/hls/?s=TOKEN` endpoint and returns highest-quality CDN HLS URL; supports tag/category/channel/pornstar shortcut queries. |
## Proxy Routes
@@ -82,6 +83,7 @@ These resolve a provider-specific input into a `302 Location`.
- `/proxy/shooshtime/{endpoint}*`
- `/proxy/pimpbunny/{endpoint}*`
- `/proxy/allpornstream/{endpoint}*`
- `/proxy/tube8/{endpoint}*`
### Media/image proxies

601
src/providers/tube8.rs Normal file
View File

@@ -0,0 +1,601 @@
use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::{Provider, build_proxy_url, report_provider_error, requester_or_default};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::parse_abbreviated_number;
use crate::util::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
use async_trait::async_trait;
use htmlentity::entity::{ICodedDataTrait, decode};
use scraper::{ElementRef, Html, Selector};
use url::form_urlencoded;
pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
crate::providers::ProviderChannelMetadata {
group_id: "mainstream-tube",
tags: &["mainstream", "studio", "search"],
};
const BASE_URL: &str = "https://www.tube8.com";
const CHANNEL_ID: &str = "tube8";
#[derive(Debug, Clone)]
pub struct Tube8Provider {
url: String,
}
#[derive(Debug, Clone)]
enum Target {
Latest,
MostViewed,
TopRated,
Search { query: String },
Tag { slug: String },
Category { slug: String },
Channel { slug: String },
Pornstar { slug: String },
}
impl Tube8Provider {
pub fn new() -> Self {
Self {
url: BASE_URL.to_string(),
}
}
fn build_channel(&self, _cv: ClientVersion) -> Channel {
Channel {
id: CHANNEL_ID.to_string(),
name: "Tube8".to_string(),
description:
"Tube8 mainstream tube with latest, most-viewed, top-rated, search, and tag/channel/pornstar shortcuts. Playback uses a signed HLS proxy."
.to_string(),
premium: false,
favicon: "https://www.google.com/s2/favicons?sz=64&domain=tube8.com".to_string(),
status: "active".to_string(),
categories: vec![],
options: vec![ChannelOption {
id: "sort".to_string(),
title: "Sort".to_string(),
description: "Browse Tube8 by sort order.".to_string(),
systemImage: "list.number".to_string(),
colorName: "blue".to_string(),
options: vec![
FilterOption {
id: "new".to_string(),
title: "Newest".to_string(),
},
FilterOption {
id: "popular".to_string(),
title: "Most Viewed".to_string(),
},
FilterOption {
id: "rated".to_string(),
title: "Top Rated".to_string(),
},
],
multiSelect: false,
}],
nsfw: true,
cacheDuration: Some(1800),
}
}
fn selector(value: &str) -> Option<Selector> {
Selector::parse(value).ok()
}
fn normalize_text(value: &str) -> String {
decode(value.as_bytes())
.to_string()
.unwrap_or_else(|_| value.to_string())
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.trim()
.to_string()
}
fn normalize_url(&self, value: &str) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
return String::new();
}
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
return trimmed.to_string();
}
if trimmed.starts_with("//") {
return format!("https:{trimmed}");
}
format!(
"{}/{}",
self.url.trim_end_matches('/'),
trimmed.trim_start_matches('/')
)
}
fn html_headers() -> Vec<(String, String)> {
vec![
(
"User-Agent".to_string(),
"Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0"
.to_string(),
),
(
"Accept".to_string(),
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
.to_string(),
),
("Accept-Language".to_string(), "en-US,en;q=0.5".to_string()),
("Referer".to_string(), format!("{BASE_URL}/")),
]
}
fn target_from_request(query: Option<&str>, sort: &str) -> Target {
if let Some(q) = query.map(str::trim).filter(|v| !v.is_empty()) {
let lower = q.to_ascii_lowercase();
for (prefix, kind) in [
("tag:", "tag"),
("category:", "category"),
("cat:", "category"),
("channel:", "channel"),
("pornstar:", "pornstar"),
("model:", "pornstar"),
] {
if let Some(rest) = lower.strip_prefix(prefix) {
let slug = rest.trim().replace(' ', "-");
if !slug.is_empty() {
return match kind {
"tag" => Target::Tag { slug },
"category" => Target::Category { slug },
"channel" => Target::Channel { slug },
_ => Target::Pornstar { slug },
};
}
}
}
return Target::Search { query: q.to_string() };
}
match sort.trim().to_ascii_lowercase().as_str() {
"popular" | "viewed" | "most_viewed" | "mv" => Target::MostViewed,
"rated" | "top" | "top_rated" | "tr" => Target::TopRated,
_ => Target::Latest,
}
}
fn build_url(&self, target: &Target, page: u16) -> String {
match target {
Target::Latest => {
// Page 1 is the home page; page 2+ use /newest/page/N/
if page > 1 {
format!("{}/newest/page/{page}/", self.url)
} else {
format!("{}/", self.url)
}
}
Target::MostViewed => {
// Page 1: /mostviewed.html; page 2+: /most-viewed/page/N/
if page > 1 {
format!("{}/most-viewed/page/{page}/", self.url)
} else {
format!("{}/mostviewed.html/", self.url)
}
}
Target::TopRated => {
// Page 1: /top.html; page 2+: /top/page/N/
if page > 1 {
format!("{}/top/page/{page}/", self.url)
} else {
format!("{}/top.html/", self.url)
}
}
Target::Search { query } => {
let encoded: String =
form_urlencoded::byte_serialize(query.as_bytes()).collect();
if page > 1 {
format!("{}/searches.html/?q={encoded}&page={page}", self.url)
} else {
format!("{}/searches.html/?q={encoded}", self.url)
}
}
Target::Tag { slug } => {
if page > 1 {
format!("{}/porntags/{slug}/?page={page}", self.url)
} else {
format!("{}/porntags/{slug}/", self.url)
}
}
Target::Category { slug } => {
if page > 1 {
format!("{}/cat/{slug}/?page={page}", self.url)
} else {
format!("{}/cat/{slug}/", self.url)
}
}
Target::Channel { slug } => {
if page > 1 {
format!("{}/channel/{slug}/?page={page}", self.url)
} else {
format!("{}/channel/{slug}/", self.url)
}
}
Target::Pornstar { slug } => {
if page > 1 {
format!("{}/pornstar/{slug}/?page={page}", self.url)
} else {
format!("{}/pornstar/{slug}/", self.url)
}
}
}
}
fn text_of(node: Option<ElementRef<'_>>) -> String {
node.map(|v| Self::normalize_text(&v.text().collect::<String>()))
.unwrap_or_default()
}
fn parse_items(&self, html: &str, options: &ServerOptions) -> Vec<VideoItem> {
let document = Html::parse_document(html);
let Some(card_sel) = Self::selector("article.video-box.js_video-box") else {
return vec![];
};
let link_sel = Self::selector("a[data-testid='plw_video_thumbnail_link']");
let title_sel = Self::selector("a.video-title-text span");
let thumb_sel = Self::selector("img.thumb-image");
let duration_sel = Self::selector(".tm_video_duration span");
let views_sel = Self::selector("span.info-views");
let uploader_sel = Self::selector("a.author-title-text");
let performer_sel = Self::selector("a.channel-performer");
let mut items = Vec::new();
for card in document.select(&card_sel) {
let id = card
.value()
.attr("data-video-id")
.map(|v| v.to_string())
.filter(|v| !v.is_empty())
.unwrap_or_default();
if id.is_empty() {
continue;
}
// Title - prefer aria-label on article; fall back to title link span
let title = card
.value()
.attr("aria-label")
.map(Self::normalize_text)
.filter(|v| !v.is_empty())
.or_else(|| {
title_sel
.as_ref()
.and_then(|s| card.select(s).next())
.map(|v| Self::normalize_text(&v.text().collect::<String>()))
.filter(|v| !v.is_empty())
})
.unwrap_or_default();
if title.is_empty() {
continue;
}
// Thumbnail
let thumb = thumb_sel
.as_ref()
.and_then(|s| card.select(s).next())
.and_then(|v| {
v.value()
.attr("data-src")
.or_else(|| v.value().attr("data-poster"))
.or_else(|| v.value().attr("src"))
})
.map(|v| self.normalize_url(v))
.unwrap_or_default();
// Preview flipbook clip from the thumbnail link's data-mediabook
let preview = link_sel
.as_ref()
.and_then(|s| card.select(s).next())
.and_then(|v| v.value().attr("data-mediabook"))
.map(|v| v.replace("&amp;", "&"))
.filter(|v| !v.is_empty() && !v.starts_with("data:"));
// Duration
let duration_text =
Self::text_of(duration_sel.as_ref().and_then(|s| card.select(s).next()));
let duration = parse_time_to_seconds(&duration_text).unwrap_or(0) as u32;
// Views (first span.info-views) and rating (second span.info-views, contains "XX%")
let all_views: Vec<_> = views_sel
.as_ref()
.map(|s| card.select(s).collect())
.unwrap_or_default();
let views = all_views
.first()
.map(|v| Self::normalize_text(&v.text().collect::<String>()))
.and_then(|t| parse_abbreviated_number(&t))
.map(|v| v as u32);
let rating = all_views
.get(1)
.map(|v| {
Self::normalize_text(&v.text().collect::<String>()).replace('%', "")
})
.and_then(|v| v.parse::<f32>().ok());
// Uploader name from article data attribute (most reliable); href from link
let uploader_name = card
.value()
.attr("data-uploader-name")
.map(Self::normalize_text)
.filter(|v| !v.is_empty())
.or_else(|| {
uploader_sel
.as_ref()
.and_then(|s| card.select(s).next())
.map(|v| Self::normalize_text(&v.text().collect::<String>()))
.filter(|v| !v.is_empty())
});
let uploader_href = uploader_sel
.as_ref()
.and_then(|s| card.select(s).next())
.and_then(|v| v.value().attr("href"))
.map(|v| self.normalize_url(v));
// Namespaced uploader ID from article attribute
let uploader_id = card
.value()
.attr("data-uploader-id")
.filter(|v| !v.is_empty())
.map(|v| format!("{CHANNEL_ID}:{v}"));
// Performer tags from channel-performer links
let mut tags: Vec<String> = Vec::new();
if let Some(sel) = &performer_sel {
for p in card.select(sel) {
let t = Self::normalize_text(&p.text().collect::<String>());
if !t.is_empty()
&& !tags.iter().any(|x: &String| x.eq_ignore_ascii_case(&t))
{
tags.push(t);
}
}
}
// Proxy URL resolves signed HLS via our redirect proxy
let proxy_url = build_proxy_url(options, CHANNEL_ID, &id);
let mut item = VideoItem::new(
id.clone(),
title,
format!("https://www.tube8.com/porn-video/{id}/"),
CHANNEL_ID.to_string(),
thumb,
duration,
);
item.views = views;
if let Some(r) = rating {
item = item.rating(r);
}
if let Some(name) = uploader_name {
item = item.uploader(name);
}
if let Some(url) = uploader_href {
item.uploaderUrl = Some(url);
}
if let Some(uid) = uploader_id {
item.uploaderId = Some(uid);
}
if let Some(p) = preview {
item = item.preview(p);
}
if !tags.is_empty() {
item = item.tags(tags);
}
item = item.formats(vec![
VideoFormat::m3u8(proxy_url, "auto".to_string(), "tube8".to_string())
.ext("mp4".to_string())
.protocol("m3u8_native".to_string())
.video_ext("mp4".to_string())
]);
items.push(item);
}
items
}
}
#[async_trait]
impl Provider for Tube8Provider {
async fn get_videos(
&self,
cache: VideoCache,
_db_pool: DbPool,
sort: String,
query: Option<String>,
page: String,
_per_page: String,
options: ServerOptions,
) -> Vec<VideoItem> {
let page = page.parse::<u16>().unwrap_or(1).max(1);
let target = Self::target_from_request(query.as_deref(), &sort);
let url = self.build_url(&target, page);
let old_items = match cache.get(&url) {
Some((time, items)) if time.elapsed().unwrap_or_default().as_secs() < 300 => {
return items.clone();
}
Some((_, items)) => items.clone(),
None => vec![],
};
let mut requester = requester_or_default(&options, CHANNEL_ID, "get_videos");
let text = match requester
.get_with_headers(&url, Self::html_headers(), None)
.await
{
Ok(v) => v,
Err(e) => {
report_provider_error(
CHANNEL_ID,
"get_videos.request",
&format!("url={url}; error={e}"),
)
.await;
return old_items;
}
};
let items = self.parse_items(&text, &options);
if items.is_empty() {
return old_items;
}
cache.remove(&url);
cache.insert(url, items.clone());
items
}
fn get_channel(&self, cv: ClientVersion) -> Option<Channel> {
Some(self.build_channel(cv))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn provider() -> Tube8Provider {
Tube8Provider::new()
}
#[test]
fn resolves_sort_targets() {
assert!(matches!(
Tube8Provider::target_from_request(None, "new"),
Target::Latest
));
assert!(matches!(
Tube8Provider::target_from_request(None, "popular"),
Target::MostViewed
));
assert!(matches!(
Tube8Provider::target_from_request(None, "rated"),
Target::TopRated
));
}
#[test]
fn resolves_prefix_shortcuts() {
let p = provider();
let _ = p;
assert!(matches!(
Tube8Provider::target_from_request(Some("tag:lesbian"), "new"),
Target::Tag { slug } if slug == "lesbian"
));
assert!(matches!(
Tube8Provider::target_from_request(Some("channel:brazzers"), "new"),
Target::Channel { slug } if slug == "brazzers"
));
assert!(matches!(
Tube8Provider::target_from_request(Some("pornstar:mia khalifa"), "new"),
Target::Pornstar { slug } if slug == "mia-khalifa"
));
assert!(matches!(
Tube8Provider::target_from_request(Some("cat:teens"), "new"),
Target::Category { slug } if slug == "teens"
));
}
#[test]
fn builds_latest_pages() {
let p = provider();
assert_eq!(p.build_url(&Target::Latest, 1), "https://www.tube8.com/");
assert_eq!(
p.build_url(&Target::Latest, 2),
"https://www.tube8.com/newest/page/2/"
);
}
#[test]
fn builds_search_pages() {
let p = provider();
let t = Target::Search { query: "teen creampie".to_string() };
assert_eq!(
p.build_url(&t, 1),
"https://www.tube8.com/searches.html/?q=teen+creampie"
);
assert_eq!(
p.build_url(&t, 2),
"https://www.tube8.com/searches.html/?q=teen+creampie&page=2"
);
}
#[test]
fn parses_video_cards() {
let p = provider();
let html = r#"
<article class="video-box pc js_video-box js-pop"
data-video-id="12345"
data-uploader-id="999"
data-uploader-name="TestChannel"
aria-label="Test Video Title">
<a href="/porn-video/12345/" data-testid="plw_video_thumbnail_link"
data-mediabook="https://ev-ph.t8cdn.com/videos/test_fb.mp4?validfrom=1&validto=2">
<div class="thumb-image-container">
<img class="thumb-image js_lazy"
data-src="https://ei-ph.t8cdn.com/videos/test/thumb.jpg"
src="data:image/png;base64,xxx"/>
</div>
<div class="video-properties">
<div class="video-duration tm_video_duration"><span>08:30</span></div>
</div>
</a>
<div class="thumb-info-wrapper">
<a class="author-title-text" href="/channel/testchannel/">TestChannel</a>
<a class="channel-performer" href="/pornstar/jane-doe/">Jane Doe</a>
<span class='info-views'>12.3K</span>
<span class='info-views'>87%</span>
</div>
</article>
"#;
let opts = ServerOptions {
featured: None,
category: None,
sites: None,
filter: None,
language: None,
public_url_base: None,
requester: None,
network: None,
stars: None,
categories: None,
duration: None,
sort: None,
sexuality: None,
};
let items = p.parse_items(html, &opts);
assert_eq!(items.len(), 1);
let item = &items[0];
assert_eq!(item.id, "12345");
assert_eq!(item.title, "Test Video Title");
assert_eq!(item.thumb, "https://ei-ph.t8cdn.com/videos/test/thumb.jpg");
assert_eq!(item.duration, 510);
assert_eq!(item.views, Some(12300));
assert_eq!(item.rating, Some(87.0));
assert_eq!(item.uploader.as_deref(), Some("TestChannel"));
assert!(item
.tags
.as_ref()
.is_some_and(|t| t.iter().any(|v| v == "Jane Doe")));
assert!(item
.preview
.as_ref()
.is_some_and(|p| p.contains("_fb.mp4")));
}
}

View File

@@ -5,6 +5,7 @@ use crate::proxies::doodstream::DoodstreamProxy;
use crate::proxies::heavyfetish::HeavyfetishProxy;
use crate::proxies::hqporner::HqpornerProxy;
use crate::proxies::pornhd3x::Pornhd3xProxy;
use crate::proxies::tube8::Tube8Proxy;
use ntex::web;
use crate::proxies::pimpbunny::PimpbunnyProxy;
@@ -37,6 +38,7 @@ pub mod shooshtime;
pub mod spankbang;
pub mod sxyprn;
pub mod thaiporntv;
pub mod tube8;
pub mod vidara;
pub mod vjav;
@@ -59,6 +61,7 @@ pub enum AnyProxy {
Vidara(VidaraProxy),
Clapdat(ClapdatProxy),
ThaipornTv(ThaipornTvProxy),
Tube8(Tube8Proxy),
}
pub trait Proxy {
@@ -85,6 +88,7 @@ impl Proxy for AnyProxy {
AnyProxy::Vidara(p) => p.get_video_url(url, requester).await,
AnyProxy::Clapdat(p) => p.get_video_url(url, requester).await,
AnyProxy::ThaipornTv(p) => p.get_video_url(url, requester).await,
AnyProxy::Tube8(p) => p.get_video_url(url, requester).await,
}
}
}

173
src/proxies/tube8.rs Normal file
View File

@@ -0,0 +1,173 @@
use ntex::web;
use crate::util::requester::Requester;
const BASE_URL: &str = "https://www.tube8.com";
#[derive(Debug, Clone)]
pub struct Tube8Proxy {}
impl Tube8Proxy {
pub fn new() -> Self {
Tube8Proxy {}
}
fn html_headers() -> Vec<(String, String)> {
vec![
(
"User-Agent".to_string(),
"Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0"
.to_string(),
),
(
"Accept".to_string(),
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string(),
),
("Accept-Language".to_string(), "en-US,en;q=0.5".to_string()),
("Referer".to_string(), format!("{BASE_URL}/")),
]
}
fn api_headers(referer: &str) -> Vec<(String, String)> {
vec![
(
"User-Agent".to_string(),
"Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0"
.to_string(),
),
("Accept".to_string(), "application/json, text/javascript, */*; q=0.01".to_string()),
("Referer".to_string(), referer.to_string()),
("X-Requested-With".to_string(), "XMLHttpRequest".to_string()),
]
}
// Extract the first /media/hls/?s=... URL from a video page.
// The page embeds it as: "videoUrl":"https:\/\/www.tube8.com\/media\/hls\/?s=TOKEN"
fn extract_hls_endpoint(html: &str) -> Option<String> {
let needle = r#""format":"hls","videoUrl":""#;
let start = html.find(needle)? + needle.len();
let rest = &html[start..];
let end = rest.find('"')?;
let raw = &rest[..end];
// JSON-escaped forward slashes → real URL
Some(raw.replace(r"\/", "/"))
}
// Parse the JSON quality array returned by /media/hls/?s=...
// Returns the highest-quality HLS master playlist URL.
fn best_hls_url(json: &str) -> Option<String> {
let parsed: serde_json::Value = serde_json::from_str(json).ok()?;
let arr = parsed.as_array()?;
// Prefer highest numeric quality; fall back to defaultQuality
let mut best_quality: i64 = -1;
let mut best_url: Option<String> = None;
let mut default_url: Option<String> = None;
for entry in arr {
let url = entry
.get("videoUrl")
.and_then(|v| v.as_str())
.map(|v| v.replace(r"\/", "/"))
.filter(|v| !v.is_empty())?;
if entry
.get("defaultQuality")
.and_then(|v| v.as_bool())
.unwrap_or(false)
&& default_url.is_none()
{
default_url = Some(url.clone());
}
if let Some(q) = entry
.get("quality")
.and_then(|v| v.as_str())
.and_then(|v| v.parse::<i64>().ok())
{
if q > best_quality {
best_quality = q;
best_url = Some(url);
}
}
}
best_url.or(default_url)
}
pub async fn get_video_url(
&self,
video_id: String,
requester: web::types::State<Requester>,
) -> String {
let video_id = video_id.trim_matches('/').trim();
if video_id.is_empty() {
return String::new();
}
let page_url = format!("{BASE_URL}/porn-video/{video_id}/");
let mut req = requester.get_ref().clone();
// Step 1: fetch video page to get the signed /media/hls/ endpoint
let html = match req
.get_with_headers(&page_url, Self::html_headers(), None)
.await
{
Ok(v) => v,
Err(_) => return String::new(),
};
let hls_endpoint = match Self::extract_hls_endpoint(&html) {
Some(url) => url,
None => return String::new(),
};
// Step 2: call the signed endpoint to get quality options
let json = match req
.get_with_headers(&hls_endpoint, Self::api_headers(&page_url), None)
.await
{
Ok(v) => v,
Err(_) => return String::new(),
};
Self::best_hls_url(&json).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::Tube8Proxy;
#[test]
fn extracts_hls_endpoint_from_page() {
let html = r#"
mediaDefinition: [{"format":"hls","videoUrl":"https:\/\/www.tube8.com\/media\/hls\/?s=eyJTOKEN","remote":true},
{"format":"mp4","videoUrl":"https:\/\/www.tube8.com\/media\/mp4\/?s=eyJTOKEN","remote":true}],
"#;
let url = Tube8Proxy::extract_hls_endpoint(html).expect("should extract");
assert_eq!(url, "https://www.tube8.com/media/hls/?s=eyJTOKEN");
}
#[test]
fn picks_best_hls_quality() {
let json = r#"[
{"defaultQuality":true,"format":"hls","quality":"480","videoUrl":"https://cdn.example/480/master.m3u8"},
{"defaultQuality":false,"format":"hls","quality":"720","videoUrl":"https://cdn.example/720/master.m3u8"},
{"defaultQuality":false,"format":"hls","quality":"1080","videoUrl":"https://cdn.example/1080/master.m3u8"},
{"defaultQuality":false,"format":"hls","quality":"240","videoUrl":"https://cdn.example/240/master.m3u8"}
]"#;
let url = Tube8Proxy::best_hls_url(json).expect("should parse");
assert_eq!(url, "https://cdn.example/1080/master.m3u8");
}
#[test]
fn falls_back_to_default_quality_when_no_numeric() {
let json = r#"[
{"defaultQuality":true,"format":"hls","videoUrl":"https://cdn.example/default/master.m3u8"},
{"defaultQuality":false,"format":"hls","videoUrl":"https://cdn.example/other/master.m3u8"}
]"#;
let url = Tube8Proxy::best_hls_url(json).expect("should parse");
assert_eq!(url, "https://cdn.example/default/master.m3u8");
}
}

View File

@@ -13,6 +13,7 @@ use crate::proxies::pornhd3x::Pornhd3xProxy;
use crate::proxies::shooshtime::ShooshtimeProxy;
use crate::proxies::spankbang::SpankbangProxy;
use crate::proxies::sxyprn::SxyprnProxy;
use crate::proxies::tube8::Tube8Proxy;
use crate::proxies::vjav::VjavProxy;
use crate::proxies::vidara::VidaraProxy;
use crate::proxies::lulustream::LulustreamProxy;
@@ -136,6 +137,11 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route(web::post().to(proxy2redirect))
.route(web::get().to(proxy2redirect)),
);
cfg.service(
web::resource("/tube8/{endpoint}*")
.route(web::post().to(proxy2redirect))
.route(web::get().to(proxy2redirect)),
);
cfg.service(
web::resource("/aps/{endpoint}*")
.route(web::post().to(crate::proxies::allpornstream::serve))
@@ -177,6 +183,7 @@ fn get_proxy(proxy: &str) -> Option<AnyProxy> {
"lulustream" => Some(AnyProxy::Lulustream(LulustreamProxy::new())),
"thaiporntv" => Some(AnyProxy::ThaipornTv(ThaipornTvProxy::new())),
"allpornstream" => Some(AnyProxy::AllPornStream(AllPornStreamProxy::new())),
"tube8" => Some(AnyProxy::Tube8(Tube8Proxy::new())),
_ => None,
}
}