shooshtime fix
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
use crate::DbPool;
|
||||
use crate::api::ClientVersion;
|
||||
use crate::providers::{Provider, report_provider_error, report_provider_error_background};
|
||||
use crate::providers::{
|
||||
Provider, build_proxy_url, report_provider_error, report_provider_error_background,
|
||||
strip_url_scheme,
|
||||
};
|
||||
use crate::status::*;
|
||||
use crate::util::cache::VideoCache;
|
||||
use crate::util::parse_abbreviated_number;
|
||||
@@ -16,6 +19,7 @@ use regex::Regex;
|
||||
use scraper::{ElementRef, Html, Selector};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::{thread, vec};
|
||||
use url::Url;
|
||||
|
||||
pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
|
||||
crate::providers::ProviderChannelMetadata {
|
||||
@@ -608,6 +612,41 @@ impl ShooshtimeProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_allowed_detail_url(&self, value: &str) -> bool {
|
||||
let normalized = self.normalize_url(value);
|
||||
let Some(url) = Url::parse(&normalized).ok() else {
|
||||
return false;
|
||||
};
|
||||
if url.scheme() != "https" {
|
||||
return false;
|
||||
}
|
||||
let Some(host) = url.host_str() else {
|
||||
return false;
|
||||
};
|
||||
(host == "shooshtime.com" || host == "www.shooshtime.com")
|
||||
&& url.path().starts_with("/videos/")
|
||||
}
|
||||
|
||||
fn proxied_video(
|
||||
&self,
|
||||
options: &ServerOptions,
|
||||
detail_url: &str,
|
||||
quality: Option<&str>,
|
||||
) -> String {
|
||||
if detail_url.is_empty() || !self.is_allowed_detail_url(detail_url) {
|
||||
return detail_url.to_string();
|
||||
}
|
||||
|
||||
let mut target = strip_url_scheme(detail_url);
|
||||
if let Some(quality) = quality.map(str::trim).filter(|quality| !quality.is_empty()) {
|
||||
target = target.trim_end_matches('/').to_string();
|
||||
target.push_str("/__quality__/");
|
||||
target.push_str(&quality.replace(' ', "%20"));
|
||||
}
|
||||
|
||||
build_proxy_url(options, "shooshtime", &target)
|
||||
}
|
||||
|
||||
fn search_sort_param(sort: &str) -> Option<&'static str> {
|
||||
match Self::normalize_sort(sort) {
|
||||
"viewed" => Some("video_viewed"),
|
||||
@@ -906,6 +945,7 @@ impl ShooshtimeProvider {
|
||||
mut item: VideoItem,
|
||||
html: &str,
|
||||
page_url: &str,
|
||||
options: &ServerOptions,
|
||||
) -> Result<VideoItem> {
|
||||
let flashvars_regex = Self::regex(r#"(?s)var\s+flashvars\s*=\s*\{(.*?)\};"#)?;
|
||||
let value_regex = |key: &str| Self::regex(&format!(r#"{key}:\s*'([^']*)'"#));
|
||||
@@ -935,17 +975,17 @@ impl ShooshtimeProvider {
|
||||
|
||||
let mut formats = Vec::new();
|
||||
if let Some(url) = &primary_url {
|
||||
let format_url = self.proxied_video(options, page_url, Some(&primary_quality));
|
||||
formats.push(
|
||||
VideoFormat::new(url.clone(), primary_quality.clone(), "mp4".to_string())
|
||||
.format_id(primary_quality.clone())
|
||||
.http_header("Referer".to_string(), page_url.to_string()),
|
||||
VideoFormat::new(format_url, primary_quality.clone(), "mp4".to_string())
|
||||
.format_id(primary_quality.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(url) = &alt_url {
|
||||
let format_url = self.proxied_video(options, page_url, Some(&alt_quality));
|
||||
formats.push(
|
||||
VideoFormat::new(url.clone(), alt_quality.clone(), "mp4".to_string())
|
||||
.format_id(alt_quality.clone())
|
||||
.http_header("Referer".to_string(), page_url.to_string()),
|
||||
VideoFormat::new(format_url, alt_quality.clone(), "mp4".to_string())
|
||||
.format_id(alt_quality.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1115,6 +1155,10 @@ impl ShooshtimeProvider {
|
||||
if let Some(title) = title {
|
||||
item.title = title;
|
||||
}
|
||||
let proxied_url = self.proxied_video(options, page_url, None);
|
||||
if !proxied_url.is_empty() {
|
||||
item.url = proxied_url;
|
||||
}
|
||||
if !formats.is_empty() {
|
||||
item = item.formats(formats);
|
||||
}
|
||||
@@ -1134,7 +1178,7 @@ impl ShooshtimeProvider {
|
||||
item = item.uploader_url(uploader_url);
|
||||
}
|
||||
if !tags.is_empty() {
|
||||
item = item.tags(tags);
|
||||
item.tags = Some(tags);
|
||||
}
|
||||
if item.preview.is_none() {
|
||||
if let Some(preview) = preview_url.as_ref() {
|
||||
@@ -1171,7 +1215,7 @@ impl ShooshtimeProvider {
|
||||
}
|
||||
};
|
||||
|
||||
match self.apply_detail_video(item, &html, &page_url) {
|
||||
match self.apply_detail_video(item, &html, &page_url, options) {
|
||||
Ok(item) => item,
|
||||
Err(error) => {
|
||||
report_provider_error_background(
|
||||
@@ -1328,7 +1372,26 @@ mod tests {
|
||||
"#;
|
||||
|
||||
let enriched = provider
|
||||
.apply_detail_video(item, html, "https://shooshtime.com/videos/example/123/")
|
||||
.apply_detail_video(
|
||||
item,
|
||||
html,
|
||||
"https://shooshtime.com/videos/example/123/",
|
||||
&crate::videos::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,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(enriched.thumb, "https://shooshtime.com/list-thumb.jpg");
|
||||
@@ -1337,4 +1400,37 @@ mod tests {
|
||||
Some("https://shooshtime.com/detail-thumb.jpg")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_proxied_video_urls() {
|
||||
let provider = ShooshtimeProvider::new();
|
||||
let options = crate::videos::ServerOptions {
|
||||
featured: None,
|
||||
category: None,
|
||||
sites: None,
|
||||
filter: None,
|
||||
language: None,
|
||||
public_url_base: Some("https://example.com".to_string()),
|
||||
requester: None,
|
||||
network: None,
|
||||
stars: None,
|
||||
categories: None,
|
||||
duration: None,
|
||||
sort: None,
|
||||
sexuality: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
provider.proxied_video(&options, "https://shooshtime.com/videos/example/123/", None,),
|
||||
"https://example.com/proxy/shooshtime/shooshtime.com/videos/example/123/"
|
||||
);
|
||||
assert_eq!(
|
||||
provider.proxied_video(
|
||||
&options,
|
||||
"https://shooshtime.com/videos/example/123/",
|
||||
Some("720p"),
|
||||
),
|
||||
"https://example.com/proxy/shooshtime/shooshtime.com/videos/example/123/__quality__/720p"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,10 @@ impl DoodstreamProxy {
|
||||
let token_regex = Self::regex(r"\b[0-9a-z]+\b")?;
|
||||
payload = token_regex
|
||||
.replace_all(&payload, |captures: &Captures| {
|
||||
let token = captures.get(0).map(|value| value.as_str()).unwrap_or_default();
|
||||
let token = captures
|
||||
.get(0)
|
||||
.map(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
let Some(index) = Self::decode_base36(token) else {
|
||||
return token.to_string();
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ pub mod pimpbunnythumb;
|
||||
pub mod porndish;
|
||||
pub mod porndishthumb;
|
||||
pub mod pornhd3x;
|
||||
pub mod shooshtime;
|
||||
pub mod spankbang;
|
||||
pub mod sxyprn;
|
||||
|
||||
|
||||
301
src/proxies/shooshtime.rs
Normal file
301
src/proxies/shooshtime.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use ntex::http::Response;
|
||||
use ntex::http::header::{CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE};
|
||||
use ntex::web::{self, HttpRequest, error};
|
||||
use regex::Regex;
|
||||
use url::Url;
|
||||
|
||||
use crate::util::requester::Requester;
|
||||
|
||||
const BASE_URL: &str = "https://shooshtime.com";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SourceCandidate {
|
||||
url: String,
|
||||
quality: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShooshtimeProxy {}
|
||||
|
||||
impl ShooshtimeProxy {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn normalize_detail_request(endpoint: &str) -> Option<(String, Option<String>)> {
|
||||
let endpoint = endpoint.trim().trim_start_matches('/');
|
||||
if endpoint.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (detail_part, quality) = match endpoint.split_once("/__quality__/") {
|
||||
Some((detail, quality)) => {
|
||||
(detail, Some(quality.replace("%20", " ").trim().to_string()))
|
||||
}
|
||||
None => (endpoint, None),
|
||||
};
|
||||
|
||||
let mut detail_url =
|
||||
if detail_part.starts_with("http://") || detail_part.starts_with("https://") {
|
||||
detail_part.to_string()
|
||||
} else {
|
||||
format!("https://{}", detail_part.trim_start_matches('/'))
|
||||
};
|
||||
|
||||
if detail_url.contains("/videos/") && !detail_url.ends_with('/') {
|
||||
detail_url.push('/');
|
||||
}
|
||||
|
||||
Self::is_allowed_detail_url(&detail_url)
|
||||
.then_some((detail_url, quality.filter(|value| !value.is_empty())))
|
||||
}
|
||||
|
||||
fn is_allowed_detail_url(url: &str) -> bool {
|
||||
let Some(url) = Url::parse(url).ok() else {
|
||||
return false;
|
||||
};
|
||||
if url.scheme() != "https" {
|
||||
return false;
|
||||
}
|
||||
let Some(host) = url.host_str() else {
|
||||
return false;
|
||||
};
|
||||
(host == "shooshtime.com" || host == "www.shooshtime.com")
|
||||
&& url.path().starts_with("/videos/")
|
||||
}
|
||||
|
||||
fn is_allowed_media_url(url: &str) -> bool {
|
||||
let Some(url) = Url::parse(url).ok() else {
|
||||
return false;
|
||||
};
|
||||
if url.scheme() != "https" {
|
||||
return false;
|
||||
}
|
||||
let Some(host) = url.host_str() else {
|
||||
return false;
|
||||
};
|
||||
(host == "shooshtime.com" || host == "www.shooshtime.com")
|
||||
&& url.path().starts_with("/get_file/")
|
||||
}
|
||||
|
||||
fn normalize_url(raw: &str) -> String {
|
||||
let value = raw.trim().replace("\\/", "/");
|
||||
if value.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
if value.starts_with("//") {
|
||||
return format!("https:{value}");
|
||||
}
|
||||
if value.starts_with('/') {
|
||||
return format!("{BASE_URL}{value}");
|
||||
}
|
||||
if value.starts_with("http://") {
|
||||
return value.replacen("http://", "https://", 1);
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
fn regex(value: &str) -> Option<Regex> {
|
||||
Regex::new(value).ok()
|
||||
}
|
||||
|
||||
fn extract_js_value(block: &str, regex: &Regex) -> Option<String> {
|
||||
regex
|
||||
.captures(block)
|
||||
.and_then(|value| value.get(1))
|
||||
.map(|value| value.as_str().replace("\\/", "/").replace("\\'", "'"))
|
||||
}
|
||||
|
||||
fn extract_sources(html: &str) -> Vec<SourceCandidate> {
|
||||
let Some(flashvars_regex) = Self::regex(r#"(?s)var\s+flashvars\s*=\s*\{(.*?)\};"#) else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(flashvars) = flashvars_regex
|
||||
.captures(html)
|
||||
.and_then(|value| value.get(1))
|
||||
.map(|value| value.as_str().to_string())
|
||||
else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let value_regex = |key: &str| Self::regex(&format!(r#"{key}:\s*'([^']*)'"#));
|
||||
let primary_url_regex = match value_regex("video_url") {
|
||||
Some(value) => value,
|
||||
None => return vec![],
|
||||
};
|
||||
let primary_quality_regex = match value_regex("video_url_text") {
|
||||
Some(value) => value,
|
||||
None => return vec![],
|
||||
};
|
||||
let alt_url_regex = match value_regex("video_alt_url") {
|
||||
Some(value) => value,
|
||||
None => return vec![],
|
||||
};
|
||||
let alt_quality_regex = match value_regex("video_alt_url_text") {
|
||||
Some(value) => value,
|
||||
None => return vec![],
|
||||
};
|
||||
|
||||
let mut sources = Vec::new();
|
||||
|
||||
if let Some(url) = Self::extract_js_value(&flashvars, &primary_url_regex) {
|
||||
let normalized = Self::normalize_url(&url);
|
||||
if !normalized.is_empty() && Self::is_allowed_media_url(&normalized) {
|
||||
sources.push(SourceCandidate {
|
||||
url: normalized,
|
||||
quality: Self::extract_js_value(&flashvars, &primary_quality_regex)
|
||||
.unwrap_or_else(|| "480p".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(url) = Self::extract_js_value(&flashvars, &alt_url_regex) {
|
||||
let normalized = Self::normalize_url(&url);
|
||||
if !normalized.is_empty() && Self::is_allowed_media_url(&normalized) {
|
||||
sources.push(SourceCandidate {
|
||||
url: normalized,
|
||||
quality: Self::extract_js_value(&flashvars, &alt_quality_regex)
|
||||
.unwrap_or_else(|| "720p".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sources
|
||||
}
|
||||
|
||||
fn quality_score(label: &str) -> u32 {
|
||||
label
|
||||
.chars()
|
||||
.filter(|value| value.is_ascii_digit())
|
||||
.collect::<String>()
|
||||
.parse::<u32>()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn select_source_url(html: &str, quality: Option<&str>) -> Option<String> {
|
||||
let sources = Self::extract_sources(html);
|
||||
if sources.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(quality) = quality {
|
||||
let wanted = quality.trim().to_ascii_lowercase();
|
||||
if let Some(source) = sources
|
||||
.iter()
|
||||
.find(|source| source.quality.trim().to_ascii_lowercase() == wanted)
|
||||
{
|
||||
return Some(source.url.clone());
|
||||
}
|
||||
}
|
||||
|
||||
sources
|
||||
.iter()
|
||||
.max_by_key(|source| Self::quality_score(&source.quality))
|
||||
.map(|source| source.url.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve_media(
|
||||
req: HttpRequest,
|
||||
requester: web::types::State<Requester>,
|
||||
) -> Result<impl web::Responder, web::Error> {
|
||||
let endpoint = req.match_info().query("endpoint").to_string();
|
||||
let Some((detail_url, quality)) = ShooshtimeProxy::normalize_detail_request(&endpoint) else {
|
||||
return Ok(web::HttpResponse::BadRequest().finish());
|
||||
};
|
||||
|
||||
let mut requester = requester.get_ref().clone();
|
||||
let html = match requester.get(&detail_url, None).await {
|
||||
Ok(html) => html,
|
||||
Err(_) => return Ok(web::HttpResponse::BadGateway().finish()),
|
||||
};
|
||||
|
||||
let Some(source_url) = ShooshtimeProxy::select_source_url(&html, quality.as_deref()) else {
|
||||
return Ok(web::HttpResponse::BadGateway().finish());
|
||||
};
|
||||
|
||||
let mut headers = vec![("Referer".to_string(), detail_url)];
|
||||
if let Some(range) = req
|
||||
.headers()
|
||||
.get("Range")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
headers.push(("Range".to_string(), range.to_string()));
|
||||
}
|
||||
|
||||
let upstream = match requester.get_raw_with_headers(&source_url, headers).await {
|
||||
Ok(response) => response,
|
||||
Err(_) => return Ok(web::HttpResponse::BadGateway().finish()),
|
||||
};
|
||||
|
||||
let status = upstream.status();
|
||||
let upstream_headers = upstream.headers().clone();
|
||||
let bytes = upstream.bytes().await.map_err(error::ErrorBadGateway)?;
|
||||
|
||||
let mut response = Response::build(status);
|
||||
if let Some(value) = upstream_headers
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
response.set_header(CONTENT_TYPE, value);
|
||||
}
|
||||
if let Some(value) = upstream_headers
|
||||
.get(CONTENT_LENGTH)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
response.set_header(CONTENT_LENGTH, value);
|
||||
}
|
||||
if let Some(value) = upstream_headers
|
||||
.get(CONTENT_RANGE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
response.set_header(CONTENT_RANGE, value);
|
||||
}
|
||||
if let Some(value) = upstream_headers
|
||||
.get("Accept-Ranges")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
response.set_header("Accept-Ranges", value);
|
||||
}
|
||||
|
||||
Ok(response.body(bytes.to_vec()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ShooshtimeProxy;
|
||||
|
||||
#[test]
|
||||
fn normalizes_detail_endpoint_and_quality() {
|
||||
let (url, quality) = ShooshtimeProxy::normalize_detail_request(
|
||||
"shooshtime.com/videos/example/123/__quality__/720p",
|
||||
)
|
||||
.expect("proxy target should parse");
|
||||
|
||||
assert_eq!(url, "https://shooshtime.com/videos/example/123/");
|
||||
assert_eq!(quality.as_deref(), Some("720p"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_requested_or_best_quality() {
|
||||
let html = r#"
|
||||
<script>
|
||||
var flashvars = {
|
||||
video_url: 'https://shooshtime.com/get_file/1/token/1/2/3.mp4/?x=1',
|
||||
video_url_text: '480p',
|
||||
video_alt_url: 'https://shooshtime.com/get_file/1/token/1/2/3_720p.mp4/?x=2',
|
||||
video_alt_url_text: '720p'
|
||||
};
|
||||
</script>
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
ShooshtimeProxy::select_source_url(html, Some("480p")).as_deref(),
|
||||
Some("https://shooshtime.com/get_file/1/token/1/2/3.mp4/?x=1")
|
||||
);
|
||||
assert_eq!(
|
||||
ShooshtimeProxy::select_source_url(html, None).as_deref(),
|
||||
Some("https://shooshtime.com/get_file/1/token/1/2/3_720p.mp4/?x=2")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,11 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.route(web::post().to(proxy2redirect))
|
||||
.route(web::get().to(proxy2redirect)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/shooshtime/{endpoint}*")
|
||||
.route(web::post().to(crate::proxies::shooshtime::serve_media))
|
||||
.route(web::get().to(crate::proxies::shooshtime::serve_media)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/pimpbunny/{endpoint}*")
|
||||
.route(web::post().to(proxy2redirect))
|
||||
|
||||
Reference in New Issue
Block a user