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:
Simon
2026-10-10 07:33:51 +00:00
parent 9bc8cf9b6a
commit aff5df31d6
2 changed files with 80 additions and 4 deletions

View File

@@ -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 | |

View File

@@ -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);