Fix thaiporntv: decode renamed data-video-sources player attribute
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi
This commit is contained in:
@@ -21,7 +21,7 @@ Status: `[ ]` open, `[~]` in progress, `[x]` fixed and verified, `[-]` not repro
|
||||
|
||||
| | Channel | Symptom | Notes |
|
||||
|---|---|---|---|
|
||||
| [ ] | `thaiporntv` | page URL "Unsupported URL", no `formats` | populate formats from detail page |
|
||||
| [x] | `thaiporntv` | page URL "Unsupported URL", no `formats` | player attribute renamed `data-enc` -> `data-video-sources` (reversed base64 JSON `{quality,url}`); decoder handles both. 3 formats/item, yt-dlp OK, check.py passes |
|
||||
| [ ] | `pornmz` | page URL "Unsupported URL", no `formats` | populate formats from detail page |
|
||||
| [ ] | `coomer` | page URL "Unsupported URL"; media host CF-gated here | check whether `formats` are populated for videos |
|
||||
| [ ] | `tube8` | page URL 404; `/proxy/tube8/{id}` 502 | |
|
||||
|
||||
@@ -406,6 +406,53 @@ impl ThaipornTvProvider {
|
||||
Ok(formats)
|
||||
}
|
||||
|
||||
/// Current player attribute: `data-video-sources` holds the base64 of a
|
||||
/// JSON array (`[{"quality":"720p","url":"..."}]`) written backwards.
|
||||
/// Falls back to the older XOR-keyed `data-enc` payload.
|
||||
fn decode_video_sources(encoded: &str) -> Result<Vec<VideoFormat>> {
|
||||
let reversed: String = encoded.trim().chars().rev().collect();
|
||||
let padding = (4 - reversed.len() % 4) % 4;
|
||||
let padded = format!("{}{}", reversed, "=".repeat(padding));
|
||||
let parsed = general_purpose::STANDARD
|
||||
.decode(&padded)
|
||||
.ok()
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
.and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
|
||||
.and_then(|json| json.as_array().cloned());
|
||||
|
||||
let Some(entries) = parsed else {
|
||||
return Self::decode_data_enc(encoded);
|
||||
};
|
||||
|
||||
let mut formats = Vec::new();
|
||||
for entry in entries {
|
||||
let Some(url) = entry
|
||||
.get("url")
|
||||
.or_else(|| entry.get("u"))
|
||||
.and_then(|v| v.as_str())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let quality = entry
|
||||
.get("quality")
|
||||
.or_else(|| entry.get("q"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("auto");
|
||||
let mut format = VideoFormat::new(
|
||||
url.to_string(),
|
||||
quality.to_string(),
|
||||
"application/x-mpegURL".to_string(),
|
||||
)
|
||||
.format_id(format!("m3u8-{quality}"));
|
||||
format.add_http_header("Referer".to_string(), BASE_URL.to_string());
|
||||
formats.push(format);
|
||||
}
|
||||
if formats.is_empty() {
|
||||
return Err(Error::from("no formats in video sources"));
|
||||
}
|
||||
Ok(formats)
|
||||
}
|
||||
|
||||
fn parse_card(&self, card: ElementRef<'_>) -> Option<VideoItem> {
|
||||
// Selectors for the Tailwind-based redesign
|
||||
let playthumb_sel = Self::selector("a.playthumb").ok()?;
|
||||
@@ -528,11 +575,15 @@ impl ThaipornTvProvider {
|
||||
.map_err(|_| Error::from(format!("detail request timed out for {}", item.url)))??;
|
||||
|
||||
let document = Html::parse_document(&detail_fetch);
|
||||
let video_element_selector = Self::selector("video[data-enc]")?;
|
||||
let video_element_selector = Self::selector("video[data-video-sources], video[data-enc]")?;
|
||||
|
||||
if let Some(video_element) = document.select(&video_element_selector).next() {
|
||||
if let Some(data_enc) = video_element.value().attr("data-enc") {
|
||||
match Self::decode_data_enc(data_enc) {
|
||||
let attr = video_element
|
||||
.value()
|
||||
.attr("data-video-sources")
|
||||
.or_else(|| video_element.value().attr("data-enc"));
|
||||
if let Some(data_enc) = attr {
|
||||
match Self::decode_video_sources(data_enc) {
|
||||
Ok(formats) => {
|
||||
item.formats = Some(formats);
|
||||
},
|
||||
@@ -731,6 +782,31 @@ mod tests {
|
||||
assert_eq!(json["http_headers"]["Referer"], BASE_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_reversed_video_sources() {
|
||||
use base64::Engine as _;
|
||||
let json = r#"[{"hd":false,"quality":"480p","url":"https://web.techvids.top/m3u8/x_480p.m3u8"},{"hd":true,"quality":"720p","url":"https://web.techvids.top/m3u8/x_720p.m3u8"}]"#;
|
||||
let encoded: String = general_purpose::STANDARD
|
||||
.encode(json)
|
||||
.trim_end_matches('=')
|
||||
.chars()
|
||||
.rev()
|
||||
.collect();
|
||||
let formats = ThaipornTvProvider::decode_video_sources(&encoded).unwrap();
|
||||
assert_eq!(formats.len(), 2);
|
||||
assert_eq!(formats[1].url, "https://web.techvids.top/m3u8/x_720p.m3u8");
|
||||
let json = serde_json::to_value(&formats[1]).unwrap();
|
||||
assert_eq!(json["quality"], "720p");
|
||||
assert_eq!(json["http_headers"]["Referer"], BASE_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_sources_falls_back_to_legacy_data_enc() {
|
||||
let encoded = "C1BwKX4pMSljf397eDEkJHxuaSV_bmhjfWJveCV_ZHskZjh-MyQ6PT4zVD8zO3slZjh-MyknKXopMSk_Mzt7KScpYykxbWpneG52Vg";
|
||||
let formats = ThaipornTvProvider::decode_video_sources(encoded).unwrap();
|
||||
assert_eq!(formats[0].url, "https://web.techvids.top/m3u8/1658_480p.m3u8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_duration() {
|
||||
assert_eq!(parse_duration_mm_ss("50:47"), 3047);
|
||||
|
||||
Reference in New Issue
Block a user