spankbang fix
This commit is contained in:
@@ -5,11 +5,13 @@ use crate::status::*;
|
|||||||
use crate::util::cache::VideoCache;
|
use crate::util::cache::VideoCache;
|
||||||
use crate::util::parse_abbreviated_number;
|
use crate::util::parse_abbreviated_number;
|
||||||
use crate::util::time::parse_time_to_seconds;
|
use crate::util::time::parse_time_to_seconds;
|
||||||
use crate::videos::{ServerOptions, VideoItem};
|
use crate::videos::{ServerOptions, VideoFormat, VideoItem};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use error_chain::error_chain;
|
use error_chain::error_chain;
|
||||||
use htmlentity::entity::{ICodedDataTrait, decode};
|
use htmlentity::entity::{ICodedDataTrait, decode};
|
||||||
use scraper::{ElementRef, Html, Selector};
|
use scraper::{ElementRef, Html, Selector};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::time::Duration;
|
||||||
use url::form_urlencoded::byte_serialize;
|
use url::form_urlencoded::byte_serialize;
|
||||||
|
|
||||||
pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
|
pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata =
|
||||||
@@ -144,6 +146,177 @@ impl SpankbangProvider {
|
|||||||
vec![("Referer".to_string(), format!("{}/", self.url))]
|
vec![("Referer".to_string(), format!("{}/", self.url))]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_cloudflare_block(text: &str) -> bool {
|
||||||
|
let lowercase = text.to_ascii_lowercase();
|
||||||
|
lowercase.contains("attention required")
|
||||||
|
|| lowercase.contains("you have been blocked")
|
||||||
|
|| lowercase.contains("cloudflare ray id")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_items_from_ytdlp(&self, page_url: &str, limit: usize) -> Vec<VideoItem> {
|
||||||
|
let output = match Command::new("yt-dlp")
|
||||||
|
.arg("-J")
|
||||||
|
.arg("--flat-playlist")
|
||||||
|
.arg("--extractor-args")
|
||||||
|
.arg("generic:impersonate=chrome")
|
||||||
|
.arg(page_url)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(output) if output.status.success() => output,
|
||||||
|
_ => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let payload: serde_json::Value = match serde_json::from_slice(&output.stdout) {
|
||||||
|
Ok(payload) => payload,
|
||||||
|
Err(_) => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let entries = match payload.get("entries").and_then(|value| value.as_array()) {
|
||||||
|
Some(entries) => entries,
|
||||||
|
None => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
for (index, entry) in entries.iter().take(limit).enumerate() {
|
||||||
|
let Some(url) = entry.get("url").and_then(|value| value.as_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !(url.starts_with("https://") || url.starts_with("http://")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = entry
|
||||||
|
.get("id")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.unwrap_or_else(|| format!("spankbang-fallback-{}", index + 1));
|
||||||
|
let title = entry
|
||||||
|
.get("title")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(Self::decode_html)
|
||||||
|
.unwrap_or_else(|| format!("SpankBang Video {}", index + 1));
|
||||||
|
let thumb = entry
|
||||||
|
.get("thumbnail")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let duration = entry
|
||||||
|
.get("duration")
|
||||||
|
.and_then(|value| value.as_u64())
|
||||||
|
.and_then(|value| u32::try_from(value).ok())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let format_kind = if url.contains(".m3u8") {
|
||||||
|
"m3u8"
|
||||||
|
} else {
|
||||||
|
"video/mp4"
|
||||||
|
};
|
||||||
|
let mut format = VideoFormat::new(url.to_string(), "auto".to_string(), format_kind.to_string());
|
||||||
|
if let Some(headers) = entry.get("http_headers").and_then(|value| value.as_object()) {
|
||||||
|
for (key, value) in headers {
|
||||||
|
if let Some(value) = value.as_str() {
|
||||||
|
format.add_http_header(key.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if entry
|
||||||
|
.get("http_headers")
|
||||||
|
.and_then(|value| value.as_object())
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
format.add_http_header("Referer".to_string(), format!("{}/", self.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut item = VideoItem::new(
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
url.to_string(),
|
||||||
|
"spankbang".to_string(),
|
||||||
|
thumb,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.formats(vec![format]);
|
||||||
|
|
||||||
|
if let Some(views) = entry
|
||||||
|
.get("view_count")
|
||||||
|
.and_then(|value| value.as_u64())
|
||||||
|
.and_then(|value| u32::try_from(value).ok())
|
||||||
|
{
|
||||||
|
item = item.views(views);
|
||||||
|
}
|
||||||
|
if let Some(uploader) = entry
|
||||||
|
.get("uploader")
|
||||||
|
.and_then(|value| value.as_str())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
item = item.uploader(uploader.to_string());
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fallback_items_with_working_media(
|
||||||
|
&self,
|
||||||
|
page_url: &str,
|
||||||
|
options: &ServerOptions,
|
||||||
|
) -> Vec<VideoItem> {
|
||||||
|
let fallback_items = self.fallback_items_from_ytdlp(page_url, 72);
|
||||||
|
if fallback_items.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut requester = requester_or_default(
|
||||||
|
options,
|
||||||
|
"spankbang",
|
||||||
|
"spankbang.fallback_items_with_working_media.missing_requester",
|
||||||
|
);
|
||||||
|
let mut working_items = Vec::new();
|
||||||
|
|
||||||
|
for item in fallback_items {
|
||||||
|
let format_headers = item
|
||||||
|
.formats
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|formats| formats.first())
|
||||||
|
.map(|format| format.http_headers_pairs())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let media_url = item
|
||||||
|
.formats
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|formats| formats.first())
|
||||||
|
.map(|format| format.url.clone())
|
||||||
|
.unwrap_or_else(|| item.url.clone());
|
||||||
|
if media_url.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut headers = format_headers;
|
||||||
|
if !headers
|
||||||
|
.iter()
|
||||||
|
.any(|(key, _)| key.eq_ignore_ascii_case("range"))
|
||||||
|
{
|
||||||
|
headers.push(("Range".to_string(), "bytes=0-2047".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_working = match requester
|
||||||
|
.get_raw_with_headers_timeout(&media_url, headers, Some(Duration::from_secs(20)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response.status().is_success(),
|
||||||
|
Err(_) => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_working {
|
||||||
|
working_items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
working_items
|
||||||
|
}
|
||||||
|
|
||||||
fn build_query_url(&self, query: &str, page: u32, sort: &str) -> String {
|
fn build_query_url(&self, query: &str, page: u32, sort: &str) -> String {
|
||||||
let encoded_query = Self::encode_search_query(query);
|
let encoded_query = Self::encode_search_query(query);
|
||||||
let mut url = if page > 1 {
|
let mut url = if page > 1 {
|
||||||
@@ -432,6 +605,14 @@ impl SpankbangProvider {
|
|||||||
&format!("url={video_url}; error={e}"),
|
&format!("url={video_url}; error={e}"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
let fallback_items = self
|
||||||
|
.fallback_items_with_working_media(&video_url, &options)
|
||||||
|
.await;
|
||||||
|
if !fallback_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), fallback_items.clone());
|
||||||
|
return Ok(fallback_items);
|
||||||
|
}
|
||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -443,6 +624,32 @@ impl SpankbangProvider {
|
|||||||
&format!("url={video_url}"),
|
&format!("url={video_url}"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
let fallback_items = self
|
||||||
|
.fallback_items_with_working_media(&video_url, &options)
|
||||||
|
.await;
|
||||||
|
if !fallback_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), fallback_items.clone());
|
||||||
|
return Ok(fallback_items);
|
||||||
|
}
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if Self::is_cloudflare_block(&text) {
|
||||||
|
report_provider_error(
|
||||||
|
"spankbang",
|
||||||
|
"get.cloudflare_block",
|
||||||
|
&format!("url={video_url}"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let fallback_items = self
|
||||||
|
.fallback_items_with_working_media(&video_url, &options)
|
||||||
|
.await;
|
||||||
|
if !fallback_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), fallback_items.clone());
|
||||||
|
return Ok(fallback_items);
|
||||||
|
}
|
||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,6 +697,14 @@ impl SpankbangProvider {
|
|||||||
&format!("url={video_url}; error={e}"),
|
&format!("url={video_url}; error={e}"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
let fallback_items = self
|
||||||
|
.fallback_items_with_working_media(&video_url, &options)
|
||||||
|
.await;
|
||||||
|
if !fallback_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), fallback_items.clone());
|
||||||
|
return Ok(fallback_items);
|
||||||
|
}
|
||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -501,6 +716,32 @@ impl SpankbangProvider {
|
|||||||
&format!("url={video_url}"),
|
&format!("url={video_url}"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
let fallback_items = self
|
||||||
|
.fallback_items_with_working_media(&video_url, &options)
|
||||||
|
.await;
|
||||||
|
if !fallback_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), fallback_items.clone());
|
||||||
|
return Ok(fallback_items);
|
||||||
|
}
|
||||||
|
return Ok(old_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if Self::is_cloudflare_block(&text) {
|
||||||
|
report_provider_error(
|
||||||
|
"spankbang",
|
||||||
|
"query.cloudflare_block",
|
||||||
|
&format!("url={video_url}"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let fallback_items = self
|
||||||
|
.fallback_items_with_working_media(&video_url, &options)
|
||||||
|
.await;
|
||||||
|
if !fallback_items.is_empty() {
|
||||||
|
cache.remove(&video_url);
|
||||||
|
cache.insert(video_url.clone(), fallback_items.clone());
|
||||||
|
return Ok(fallback_items);
|
||||||
|
}
|
||||||
return Ok(old_items);
|
return Ok(old_items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user