From aff5df31d62d36c548d6824aab5a0d2973f87347 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 10 Oct 2026 07:33:51 +0000 Subject: [PATCH] Fix thaiporntv: decode renamed data-video-sources player attribute Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MUYdCh2nCVsQYgJAT7gwSi --- docs/provider-fix-todo.md | 2 +- src/providers/thaiporntv.rs | 82 +++++++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/docs/provider-fix-todo.md b/docs/provider-fix-todo.md index 4f576fc..18c98a5 100644 --- a/docs/provider-fix-todo.md +++ b/docs/provider-fix-todo.md @@ -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 | | diff --git a/src/providers/thaiporntv.rs b/src/providers/thaiporntv.rs index 5bd7004..e6ce860 100644 --- a/src/providers/thaiporntv.rs +++ b/src/providers/thaiporntv.rs @@ -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> { + 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::(&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 { // 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);