redtube fix

This commit is contained in:
Simon
2026-04-07 12:32:41 +00:00
parent a7e38c97a6
commit 81e8158161
5 changed files with 160 additions and 111 deletions

View File

@@ -685,6 +685,7 @@ pub fn requester_or_default(
}
}
#[allow(dead_code)]
pub fn strip_url_scheme(url: &str) -> String {
url.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
@@ -693,6 +694,7 @@ pub fn strip_url_scheme(url: &str) -> String {
.to_string()
}
#[allow(dead_code)]
pub fn build_proxy_url(options: &ServerOptions, proxy: &str, target: &str) -> String {
let target = target.trim_start_matches('/');
let base = options

View File

@@ -144,11 +144,99 @@ impl RedtubeProvider {
Ok(video_items)
}
fn extract_between<'a>(&self, text: &'a str, start: &str, end: &str) -> Option<&'a str> {
let start_idx = text.find(start)?;
let from = start_idx + start.len();
let rest = &text[from..];
let end_idx = rest.find(end)?;
Some(&rest[..end_idx])
}
fn parse_video_grid_items(&self, html: &str) -> Vec<VideoItem> {
if !html.contains("videos_grid") {
return vec![];
}
let listing = html
.split("videos_grid")
.nth(1)
.unwrap_or_default()
.split("</ul>")
.next()
.unwrap_or_default();
let mut items: Vec<VideoItem> = Vec::new();
for li in listing.split("<li id=\"").skip(1) {
let id = self
.extract_between(li, "data-video-id=\"", "\"")
.unwrap_or_default()
.trim()
.to_string();
if id.is_empty() {
continue;
}
let title = li
.split("video-title-wrapper")
.nth(1)
.and_then(|part| self.extract_between(part, "title=\"", "\""))
.or_else(|| {
li.split("class=\"video-title-text")
.nth(1)
.and_then(|part| self.extract_between(part, "title=\"", "\""))
})
.or_else(|| self.extract_between(li, "<a title=\"", "\""))
.unwrap_or_default()
.trim()
.to_string();
let title = decode(title.as_bytes()).to_string().unwrap_or(title);
let thumb = self
.extract_between(li, "data-src=\"", "\"")
.or_else(|| self.extract_between(li, "data-o_thumb=\"", "\""))
.unwrap_or_default()
.replace("&amp;", "&");
let raw_duration = self
.extract_between(li, "<span class=\"video-properties tm_video_duration\">", "</span>")
.unwrap_or_default()
.trim()
.to_string();
let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32;
let views_str = self
.extract_between(li, "<span class='info-views'>", "</span>")
.unwrap_or_default()
.trim()
.to_string();
let views = parse_abbreviated_number(&views_str).unwrap_or(0) as u32;
let preview = self
.extract_between(li, "data-mediabook=\"", "\"")
.unwrap_or_default()
.replace("&amp;", "&");
let video_url = format!("{}/{}", self.url, id);
let video_item =
VideoItem::new(id, title, video_url, "redtube".to_string(), thumb, duration)
.views(views)
.preview(preview);
items.push(video_item);
}
items
}
fn get_video_items_from_html(&self, html: String) -> Vec<VideoItem> {
if html.is_empty() {
println!("HTML is empty");
return vec![];
}
let card_items = self.parse_video_grid_items(&html);
if !card_items.is_empty() {
return card_items;
}
let mut items: Vec<VideoItem> = Vec::new();
let video_listing_content = html
.split("<script type=\"application/ld+json\">")
@@ -220,117 +308,7 @@ impl RedtubeProvider {
println!("HTML is empty");
return vec![];
}
let mut items: Vec<VideoItem> = Vec::new();
let video_listing_content = html
.split("videos_grid")
.collect::<Vec<&str>>()
.get(1)
.copied()
.unwrap_or_default();
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>>()
.get(1)
.copied()
.unwrap_or_default()
.split("\"")
.collect::<Vec<&str>>()
.get(0)
.copied()
.unwrap_or_default()
.to_string();
let video_url = format!("{}/{}", self.url, id);
let title = vid
.split(" <a title=\"")
.collect::<Vec<&str>>()
.get(1)
.copied()
.unwrap_or_default()
.split("\"")
.collect::<Vec<&str>>()
.get(0)
.copied()
.unwrap_or_default()
.trim()
.to_string();
let thumb = vid
.split("<img")
.collect::<Vec<&str>>()
.get(1)
.copied()
.unwrap_or_default()
.split(" data-src=\"")
.collect::<Vec<&str>>()
.get(1)
.copied()
.unwrap_or_default()
.split("\"")
.collect::<Vec<&str>>()
.get(0)
.copied()
.unwrap_or_default()
.to_string();
let raw_duration = vid
.split("<span class=\"video-properties tm_video_duration\">")
.collect::<Vec<&str>>()
.get(1)
.copied()
.unwrap_or_default()
.split("</span>")
.collect::<Vec<&str>>()
.get(0)
.copied()
.unwrap_or_default()
.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>>()
.get(1)
.copied()
.unwrap_or_default()
.split("</span>")
.collect::<Vec<&str>>()
.get(0)
.copied()
.unwrap_or_default()
.trim()
.to_string();
let views = parse_abbreviated_number(&views_str).unwrap_or(0) as u32;
let preview = vid
.split("<img")
.collect::<Vec<&str>>()
.get(1)
.copied()
.unwrap_or_default()
.split(" data-mediabook=\"")
.collect::<Vec<&str>>()
.get(1)
.copied()
.unwrap_or_default()
.split("\"")
.collect::<Vec<&str>>()
.get(0)
.copied()
.unwrap_or_default()
.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;
self.parse_video_grid_items(&html)
}
}
@@ -376,3 +354,63 @@ impl Provider for RedtubeProvider {
Some(self.build_channel(clientversion))
}
}
#[cfg(test)]
mod tests {
use super::RedtubeProvider;
#[test]
fn parse_video_grid_items_handles_browse_cards() {
let provider = RedtubeProvider::new();
let html = r#"
<ul id="block_browse" class="videos_grid">
<li id="browse_195840661" data-video-id="195840661">
<a data-testid="plw_video_thumbnail_link" href="/195840661" data-video-id="195840661">
<img data-src="https://cdn.example/thumb.jpg" data-mediabook="https://cdn.example/preview.mp4?x=1&amp;y=2">
</a>
<a class="video-title-text js-pop tm_video_title " title="Stepmoms &amp; More"></a>
<span class="video-properties tm_video_duration">2:17:57</span>
<span class='info-views'>981K</span>
</li>
</ul>
"#;
let items = provider.parse_video_grid_items(html);
assert_eq!(items.len(), 1);
assert_eq!(items[0].id, "195840661");
assert_eq!(items[0].title, "Stepmoms & More");
assert_eq!(items[0].url, "https://www.redtube.com/195840661");
assert_eq!(items[0].thumb, "https://cdn.example/thumb.jpg");
assert_eq!(
items[0].preview.as_deref(),
Some("https://cdn.example/preview.mp4?x=1&y=2")
);
assert_eq!(items[0].duration, 8277);
assert_eq!(items[0].views, Some(981000));
}
#[test]
fn parse_video_grid_items_handles_tags_cards() {
let provider = RedtubeProvider::new();
let html = r#"
<div><ul class="videos_grid">
<li id="tags_videos_42785231" data-video-id="42785231">
<a data-testid="plw_video_thumbnail_link" href="/42785231" data-video-id="42785231">
<img data-o_thumb="https://cdn.example/thumb2.jpg" data-mediabook="https://cdn.example/p2.mp4">
</a>
<a class="video-title-text js-pop tm_video_title " title="Title 2"></a>
<span class="video-properties tm_video_duration">13:06</span>
<span class='info-views'>51.2K</span>
</li>
</ul></div>
"#;
let items = provider.parse_video_grid_items(html);
assert_eq!(items.len(), 1);
assert_eq!(items[0].id, "42785231");
assert_eq!(items[0].url, "https://www.redtube.com/42785231");
assert_eq!(items[0].thumb, "https://cdn.example/thumb2.jpg");
assert_eq!(items[0].duration, 786);
assert_eq!(items[0].views, Some(51200));
}
}

View File

@@ -82,6 +82,7 @@ pub struct UploaderVideoRef {
}
impl UploaderVideoRef {
#[allow(dead_code)]
pub fn from_video_item(item: &VideoItem, uploader_name: &str, uploader_id: &str) -> Self {
Self {
id: item.id.clone(),
@@ -117,6 +118,7 @@ pub struct UploaderLayoutRow {
}
impl UploaderLayoutRow {
#[allow(dead_code)]
pub fn horizontal(title: Option<String>, video_ids: Vec<String>) -> Self {
Self {
rowType: UploaderLayoutRowType::Horizontal,
@@ -125,6 +127,7 @@ impl UploaderLayoutRow {
}
}
#[allow(dead_code)]
pub fn videos(title: Option<String>) -> Self {
Self {
rowType: UploaderLayoutRowType::Videos,
@@ -150,12 +153,14 @@ pub fn normalize_optional_string(value: Option<String>) -> Option<String> {
})
}
#[allow(dead_code)]
pub fn iso_timestamp_from_unix(value: Option<u64>) -> Option<String> {
let timestamp = value?;
let dt = Utc.timestamp_opt(timestamp as i64, 0).single()?;
Some(dt.to_rfc3339_opts(SecondsFormat::Millis, true))
}
#[allow(dead_code)]
fn normalize_rating(value: f32) -> u32 {
value.clamp(0.0, 100.0).round() as u32
}

View File

@@ -10,6 +10,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
static ERROR_CACHE: Lazy<DashMap<String, u64>> = Lazy::new(DashMap::new);
// const COOLDOWN_SECONDS: u64 = 3600; // 1 Hour cooldown
#[allow(dead_code)]
pub fn format_error_chain(err: &dyn Error) -> String {
let mut chain_str = String::new();
let mut current_err: Option<&dyn Error> = Some(err);

View File

@@ -3,6 +3,7 @@ use url::Url;
use crate::providers::{build_proxy_url, strip_url_scheme};
use crate::videos::ServerOptions;
#[allow(dead_code)]
const DOODSTREAM_HOSTS: &[&str] = &[
"turboplayers.xyz",
"www.turboplayers.xyz",
@@ -12,6 +13,7 @@ const DOODSTREAM_HOSTS: &[&str] = &[
"www.streamhg.com",
];
#[allow(dead_code)]
pub fn proxy_name_for_url(url: &str) -> Option<&'static str> {
let parsed = Url::parse(url).ok()?;
let host = parsed.host_str()?.to_ascii_lowercase();
@@ -23,6 +25,7 @@ pub fn proxy_name_for_url(url: &str) -> Option<&'static str> {
None
}
#[allow(dead_code)]
pub fn rewrite_hoster_url(options: &ServerOptions, url: &str) -> String {
match proxy_name_for_url(url) {
Some(proxy_name) => build_proxy_url(options, proxy_name, &strip_url_scheme(url)),