thaiporntv: rewrite provider for Tailwind redesign and fix base64 decode

The site was redesigned from old HTML classes to Tailwind CSS, breaking all
selectors. Also fixes a base64 space-padding bug that corrupted the XOR cipher
decryption of data-enc attributes (video stream URLs).

Key changes:
- New parse_card() using updated Tailwind CSS selectors (div.group, a.playthumb,
  a.text-brand-pink, etc.) to match the redesigned page structure
- Fixed base64 padding from spaces to = characters in both provider and proxy
- Fixed proxy route (/proxy/thaiporntv/{endpoint}* was double-prefixed and used
  wrong capture group name)
- Updated load_tags() to use a.group[href*='/tags/'] with h2 child selector
- Added CDN base URL constant (web.techvids.top) for thumbnail and HLS paths
- Preview GIF URLs populated from data-id attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon
2026-05-18 18:10:56 +00:00
parent aea2cda627
commit dc14adbb2e
3 changed files with 152 additions and 172 deletions

View File

@@ -3,13 +3,11 @@ use crate::api::ClientVersion;
use crate::providers::{Provider, report_provider_error, report_provider_error_background, requester_or_default}; use crate::providers::{Provider, report_provider_error, report_provider_error_background, requester_or_default};
use crate::status::*; use crate::status::*;
use crate::util::cache::VideoCache; use crate::util::cache::VideoCache;
use crate::util::parse_abbreviated_number;
use crate::util::requester::Requester; use crate::util::requester::Requester;
use crate::util::time::parse_time_to_seconds;
use crate::videos::{ServerOptions, VideoFormat, VideoItem}; use crate::videos::{ServerOptions, VideoFormat, VideoItem};
use async_trait::async_trait; use async_trait::async_trait;
use base64::{engine::general_purpose, Engine}; use base64::{engine::general_purpose, Engine};
use chrono::{DateTime, Duration as ChronoDuration, NaiveDate, Utc}; use chrono::{DateTime, NaiveDate, Utc};
use error_chain::error_chain; use error_chain::error_chain;
use futures::stream::{self, StreamExt}; use futures::stream::{self, StreamExt};
use htmlentity::entity::{ICodedDataTrait, decode}; use htmlentity::entity::{ICodedDataTrait, decode};
@@ -47,6 +45,7 @@ const USER_AGENT: &str =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"; "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
const HTML_ACCEPT: &str = const HTML_ACCEPT: &str =
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"; "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";
const CDN_BASE: &str = "https://web.techvids.top";
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ThaipornTvProvider { pub struct ThaipornTvProvider {
@@ -66,9 +65,6 @@ enum ArchiveMode {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Target { enum Target {
Archive(ArchiveMode), Archive(ArchiveMode),
Search {
query: String,
},
Tag { Tag {
slug: String, slug: String,
}, },
@@ -181,10 +177,6 @@ impl ThaipornTvProvider {
.map_err(|error| Error::from(format!("selector `{value}` parse failed: {error}"))) .map_err(|error| Error::from(format!("selector `{value}` parse failed: {error}")))
} }
fn regex(value: &str) -> Result<Regex> {
Regex::new(value).map_err(|error| Error::from(format!("regex `{value}` failed: {error}")))
}
fn collapse_whitespace(text: &str) -> String { fn collapse_whitespace(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ") text.split_whitespace().collect::<Vec<_>>().join(" ")
} }
@@ -250,20 +242,28 @@ impl ThaipornTvProvider {
url: base_url.to_string(), url: base_url.to_string(),
tags: Arc::clone(&tags), tags: Arc::clone(&tags),
}; };
let html = provider.fetch_html(&mut requester, &format!("{}/tags/", base_url), &format!("{}/", base_url)).await?; let html = provider.fetch_html(
&mut requester,
&format!("{}/tags/", base_url),
&format!("{}/", base_url),
).await?;
let document = Html::parse_document(&html); let document = Html::parse_document(&html);
let selector = Self::selector("a[href*='/tags/']")?; // Tag cards are <a class="group block relative ..."> links with /tags/ in href
for element in document.select(&selector) { // html5ever handles unquoted href attributes correctly
let a_selector = Self::selector("a.group[href*='/tags/']")?;
let h2_selector = Self::selector("h2")?;
for element in document.select(&a_selector) {
let Some(href) = element.value().attr("href") else { let Some(href) = element.value().attr("href") else {
continue; continue;
}; };
let title = Self::decode_html_entities(&element.text().collect::<String>()); // Skip pagination and root tag page
let re = Regex::new(r"^(.+?)\s+\d+$").unwrap(); // Remove count from tag title if href.ends_with("/tags/") || href.contains("/page/") {
let title = if let Some(captures) = re.captures(&title) { continue;
captures.get(1).unwrap().as_str().to_string() }
} else { // Extract title from the h2 inside the card
title let title = element.select(&h2_selector).next()
}; .map(|h| Self::collapse_whitespace(&h.text().collect::<String>()))
.unwrap_or_default();
if title.is_empty() { if title.is_empty() {
continue; continue;
} }
@@ -288,7 +288,14 @@ impl ThaipornTvProvider {
} }
} }
fn resolve_option_target(&self, options: &ServerOptions, sort: &str) -> Target { fn resolve_target(&self, options: &ServerOptions, sort: &str, query: Option<&str>) -> Target {
// Query: check for tag shortcut first
if let Some(q) = query {
if let Some(target) = self.find_tag_target_in_options(q) {
return target;
}
}
// Filter option: check for tag shortcut
if let Some(value) = options.filter.as_deref() { if let Some(value) = options.filter.as_deref() {
if let Some(target) = self.find_tag_target_in_options(value) { if let Some(target) = self.find_tag_target_in_options(value) {
return target; return target;
@@ -297,19 +304,7 @@ impl ThaipornTvProvider {
Target::Archive(Self::archive_from_sort(sort)) Target::Archive(Self::archive_from_sort(sort))
} }
fn resolve_query_target(&self, query: &str) -> Target { fn find_tag_target_in_options(&self, value: &str) -> Option<Target> {
if let Some(target) = self.find_tag_target_in_options(query) {
return target;
}
Target::Search {
query: query.trim().to_string(),
}
}
fn find_tag_target_in_options(
&self,
value: &str,
) -> Option<Target> {
let normalized = value.trim().to_lowercase(); let normalized = value.trim().to_lowercase();
let tags = self.tags.read().ok()?; let tags = self.tags.read().ok()?;
let option = tags.iter().find(|item| { let option = tags.iter().find(|item| {
@@ -321,8 +316,14 @@ impl ThaipornTvProvider {
fn target_from_filter_id(&self, id: &str) -> Option<Target> { fn target_from_filter_id(&self, id: &str) -> Option<Target> {
if id.contains("/tags/") { if id.contains("/tags/") {
let url = Url::parse(&self.absolute_url(id)).ok()?; let url = Url::parse(&self.absolute_url(id)).ok()?;
let path_segments = url.path_segments()?; let segments: Vec<_> = url
let slug = path_segments.last()?.trim_end_matches('/').to_string(); .path_segments()?
.filter(|s| !s.is_empty())
.collect();
let slug = segments.last()?.to_string();
if slug == "tags" {
return None;
}
return Some(Target::Tag { slug }); return Some(Target::Tag { slug });
} }
None None
@@ -331,7 +332,6 @@ impl ThaipornTvProvider {
fn build_url_for_target(&self, target: &Target, page: u32) -> String { fn build_url_for_target(&self, target: &Target, page: u32) -> String {
match target { match target {
Target::Archive(mode) => self.build_archive_url(*mode, page), Target::Archive(mode) => self.build_archive_url(*mode, page),
Target::Search { query } => self.build_search_url(query, page),
Target::Tag { slug } => self.build_tag_url(slug, page), Target::Tag { slug } => self.build_tag_url(slug, page),
} }
} }
@@ -351,15 +351,6 @@ impl ThaipornTvProvider {
} }
} }
fn build_search_url(&self, query: &str, page: u32) -> String {
let encoded_query = utf8_percent_encode(query, NON_ALPHANUMERIC).to_string();
if page <= 1 {
format!("{}/search/?q={}", self.url, encoded_query)
} else {
format!("{}/search/?q={}&page={}", self.url, encoded_query, page)
}
}
fn build_tag_url(&self, slug: &str, page: u32) -> String { fn build_tag_url(&self, slug: &str, page: u32) -> String {
let encoded_slug = utf8_percent_encode(slug, NON_ALPHANUMERIC).to_string(); let encoded_slug = utf8_percent_encode(slug, NON_ALPHANUMERIC).to_string();
if page <= 1 { if page <= 1 {
@@ -371,7 +362,8 @@ impl ThaipornTvProvider {
fn decode_data_enc(encoded_data: &str) -> Result<Vec<VideoFormat>> { fn decode_data_enc(encoded_data: &str) -> Result<Vec<VideoFormat>> {
let cleaned_data = encoded_data.replace("-", "+").replace("_", "/"); let cleaned_data = encoded_data.replace("-", "+").replace("_", "/");
let padded_data = format!("{:<pad$}", cleaned_data, pad = (cleaned_data.len() + 3) & !3); let padding = (4 - cleaned_data.len() % 4) % 4;
let padded_data = format!("{}{}", cleaned_data, "=".repeat(padding));
let decoded_bytes = general_purpose::STANDARD.decode(&padded_data) let decoded_bytes = general_purpose::STANDARD.decode(&padded_data)
.map_err(|e| Error::from(format!("Base64 decode failed: {e}")))?; .map_err(|e| Error::from(format!("Base64 decode failed: {e}")))?;
@@ -404,9 +396,8 @@ impl ThaipornTvProvider {
let mut format = VideoFormat::new( let mut format = VideoFormat::new(
u.to_string(), u.to_string(),
q.to_string(), q.to_string(),
"application/x-mpegURL".to_string(), // Assuming m3u8 "application/x-mpegURL".to_string(),
); );
// Add referer to the format
format.add_http_header("Referer".to_string(), BASE_URL.to_string()); format.add_http_header("Referer".to_string(), BASE_URL.to_string());
formats.push(format); formats.push(format);
} }
@@ -414,77 +405,91 @@ impl ThaipornTvProvider {
Ok(formats) Ok(formats)
} }
fn parse_card( fn parse_card(&self, card: ElementRef<'_>) -> Option<VideoItem> {
&self, // Selectors for the Tailwind-based redesign
card: ElementRef<'_>, let playthumb_sel = Self::selector("a.playthumb").ok()?;
_proxy_base_url: &str, let img_sel = Self::selector("img").ok()?;
) -> Option<VideoItem> { let title_sel = Self::selector("a.text-brand-pink").ok()?;
let id_selector = Self::selector("a[href*='/videos/']").ok()?; let tag_sel = Self::selector("a[href*='/tags/']").ok()?;
let title_selector = Self::selector("a[href*='/videos/']").ok()?; let date_sel = Self::selector("span.ml-auto").ok()?;
let thumb_selector = Self::selector("img").ok()?;
let duration_selector = Self::selector("div.duration").ok()?;
let views_selector = Self::selector("div.views").ok()?;
let uploaded_at_selector = Self::selector("div.date").ok()?;
let tag_selector = Self::selector("a[href*='/tags/']").ok()?;
let href_element = card.select(&id_selector).next()?; let link = card.select(&playthumb_sel).next()?;
let href = href_element.value().attr("href")?.to_string(); let href = link.value().attr("href")?;
let data_id = link.value().attr("data-id").unwrap_or("");
let re = Regex::new(r"/videos/\d{4}/[^/-]+-(\d+)/$").unwrap(); // ID: numeric part from data-id (xn88-39688 → 39688) or from URL
let captures = re.captures(&href)?; let id = if !data_id.is_empty() {
let id = captures.get(1)?.as_str().to_string(); data_id.rsplit('-').next().unwrap_or(data_id).to_string()
} else {
let re = Regex::new(r"-(\d+)/$").unwrap();
re.captures(href)?.get(1)?.as_str().to_string()
};
let title = card.select(&title_selector).next() let url = if href.starts_with("http") {
.and_then(|e| e.value().attr("title")) href.to_string()
.map(Self::decode_html_entities) } else {
.unwrap_or_else(|| { self.absolute_url(href)
card.select(&thumb_selector).next() };
.and_then(|e| e.value().attr("alt"))
.map(Self::decode_html_entities)
.unwrap_or_default()
});
let thumb = card.select(&thumb_selector).next() let thumb = card.select(&img_sel).next()
.and_then(|e| e.value().attr("src")) .and_then(|e| e.value().attr("src"))
.map(|s| self.absolute_url(s)) .map(|s| if s.starts_with("http") { s.to_string() } else { self.absolute_url(s) })
.unwrap_or_default(); .unwrap_or_default();
let duration_text = card.select(&duration_selector).next() // Preview GIF from CDN
.map(|e| Self::collapse_whitespace(&e.text().collect::<String>())) let preview = if !data_id.is_empty() {
.unwrap_or_default(); Some(format!("{CDN_BASE}/2/4/7/9/preview/{data_id}_preview.gif"))
let duration = parse_time_to_seconds(&duration_text).unwrap_or(0) as u32; } else {
None
};
let views = card.select(&views_selector).next() let title = card.select(&title_sel).next()
.map(|e| Self::collapse_whitespace(&e.text().collect::<String>()))
.and_then(|s| s.strip_suffix(" views").map(|s| parse_abbreviated_number(s)))
.flatten();
let uploaded_at_text = card.select(&uploaded_at_selector).next()
.map(|e| Self::collapse_whitespace(&e.text().collect::<String>())) .map(|e| Self::collapse_whitespace(&e.text().collect::<String>()))
.filter(|t| !t.is_empty())
.or_else(|| {
link.value().attr("aria-label")
.map(|s| {
let s = s.strip_prefix("Watch ").unwrap_or(s);
let s = s.strip_suffix(" video").unwrap_or(s);
Self::decode_html_entities(s)
})
})
.unwrap_or_default(); .unwrap_or_default();
let uploaded_at = NaiveDate::parse_from_str(&uploaded_at_text, "%d %b %Y")
.ok() // Duration is in a font-mono div inside the thumbnail overlay
let card_html = card.html();
let dur_re = Regex::new(r"font-mono[^>]+>(\d+:\d+(?::\d+)?)<").unwrap();
let duration_text = dur_re.captures(&card_html)
.and_then(|c| c.get(1))
.map(|m| m.as_str().to_string())
.unwrap_or_default();
let duration = parse_duration_mm_ss(&duration_text);
// Views from the fa-eye span
let views_re = Regex::new(r"fa-eye[^>]+></i>\s*(\d[\d,]*)").unwrap();
let views = views_re.captures(&card_html)
.and_then(|c| c.get(1))
.and_then(|m| m.as_str().replace(',', "").parse::<u32>().ok());
// Upload date from the ml-auto span
let uploaded_at = card.select(&date_sel).next()
.map(|e| Self::collapse_whitespace(&e.text().collect::<String>()))
.and_then(|s| NaiveDate::parse_from_str(s.trim(), "%d %b %Y").ok())
.and_then(|date| { .and_then(|date| {
date.and_hms_opt(0, 0, 0) date.and_hms_opt(0, 0, 0)
.map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc).timestamp() as u64) .map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc).timestamp() as u64)
}); });
let tags: Vec<String> = card.select(&tag_selector) // Tags from /tags/ links in the card (these are simple text-only links in cards)
.filter_map(|e| e.value().attr("href")) let tags: Vec<String> = card.select(&tag_sel)
.filter_map(|link_href| { .map(|e| Self::collapse_whitespace(&e.text().collect::<String>()))
Url::parse(&self.absolute_url(link_href)) .filter(|s| !s.is_empty())
.ok()
.and_then(|url| url.path_segments().map(|segments| segments.map(ToString::to_string).collect::<Vec<String>>()))
.and_then(|segments_vec| segments_vec.last().cloned())
.map(|s| Self::decode_html_entities(&s).trim_end_matches('/').to_string())
})
.collect(); .collect();
let mut item = VideoItem::new( let mut item = VideoItem::new(
id, id,
title, title,
self.absolute_url(&href), url,
CHANNEL_ID.to_string(), CHANNEL_ID.to_string(),
thumb, thumb,
duration, duration,
@@ -492,18 +497,20 @@ impl ThaipornTvProvider {
if let Some(views) = views { item = item.views(views); } if let Some(views) = views { item = item.views(views); }
if let Some(uploaded_at) = uploaded_at { item = item.uploaded_at(uploaded_at); } if let Some(uploaded_at) = uploaded_at { item = item.uploaded_at(uploaded_at); }
if let Some(preview) = preview { item = item.preview(preview); }
if !tags.is_empty() { item = item.tags(tags); } if !tags.is_empty() { item = item.tags(tags); }
Some(item) Some(item)
} }
fn get_video_items_from_html(&self, html: String, proxy_base_url: &str) -> Result<Vec<VideoItem>> { fn get_video_items_from_html(&self, html: String) -> Result<Vec<VideoItem>> {
let document = Html::parse_document(&html); let document = Html::parse_document(&html);
let card_selector = Self::selector("div.video-list-item")?; // Cards use class "group flex flex-col"; ad cards additionally have "ad-container"
let card_selector = Self::selector("div.group:not(.ad-container)")?;
let mut items = Vec::new(); let mut items = Vec::new();
for card in document.select(&card_selector) { for card in document.select(&card_selector) {
if let Some(item) = self.parse_card(card, proxy_base_url) { if let Some(item) = self.parse_card(card) {
items.push(item); items.push(item);
} }
} }
@@ -529,7 +536,11 @@ impl ThaipornTvProvider {
item.formats = Some(formats); item.formats = Some(formats);
}, },
Err(e) => { Err(e) => {
report_provider_error_background(CHANNEL_ID, "decode_data_enc", &format!("url={}; error={}", item.url, e)); report_provider_error_background(
CHANNEL_ID,
"decode_data_enc",
&format!("url={}; error={}", item.url, e),
);
} }
} }
} }
@@ -560,7 +571,7 @@ impl ThaipornTvProvider {
.await .await
.map_err(|_| Error::from(format!("list request timed out for {url}")))??; .map_err(|_| Error::from(format!("list request timed out for {url}")))??;
let list_items = self.get_video_items_from_html(html, options.public_url_base.as_deref().unwrap_or_default())?; let list_items = self.get_video_items_from_html(html)?;
if list_items.is_empty() { if list_items.is_empty() {
return Ok(vec![]); return Ok(vec![]);
@@ -605,27 +616,28 @@ impl ThaipornTvProvider {
cache: VideoCache, cache: VideoCache,
page: u32, page: u32,
sort: &str, sort: &str,
query: Option<&str>,
per_page_limit: usize, per_page_limit: usize,
options: ServerOptions, options: ServerOptions,
) -> Result<Vec<VideoItem>> { ) -> Result<Vec<VideoItem>> {
let target = self.resolve_option_target(&options, sort); let target = self.resolve_target(&options, sort, query);
let url = self.build_url_for_target(&target, page); let url = self.build_url_for_target(&target, page);
self.fetch_items_for_url(cache, url, per_page_limit, page <= 1, &options) self.fetch_items_for_url(cache, url, per_page_limit, page <= 1, &options)
.await .await
} }
}
async fn query( /// Parse "MM:SS" or "HH:MM:SS" into total seconds.
&self, fn parse_duration_mm_ss(text: &str) -> u32 {
cache: VideoCache, let parts: Vec<u32> = text
page: u32, .split(':')
query: &str, .filter_map(|p| p.trim().parse().ok())
per_page_limit: usize, .collect();
options: ServerOptions, match parts.as_slice() {
) -> Result<Vec<VideoItem>> { [h, m, s] => h * 3600 + m * 60 + s,
let target = self.resolve_query_target(query); [m, s] => m * 60 + s,
let url = self.build_url_for_target(&target, page); [s] => *s,
self.fetch_items_for_url(cache, url, per_page_limit, page <= 1, &options) _ => 0,
.await
} }
} }
@@ -644,14 +656,9 @@ impl Provider for ThaipornTvProvider {
let _ = pool; let _ = pool;
let page = page.parse::<u32>().unwrap_or(1); let page = page.parse::<u32>().unwrap_or(1);
let per_page_limit = per_page.parse::<usize>().unwrap_or(30); let per_page_limit = per_page.parse::<usize>().unwrap_or(30);
let query_ref = query.as_deref().filter(|q| !q.trim().is_empty());
let result = match query { let result = self.get(cache, page, &sort, query_ref, per_page_limit, options).await;
Some(query) if !query.trim().is_empty() => {
self.query(cache, page, &query, per_page_limit, options)
.await
}
_ => self.get(cache, page, &sort, per_page_limit, options).await,
};
match result { match result {
Ok(videos) => videos, Ok(videos) => videos,
@@ -698,19 +705,6 @@ mod tests {
); );
} }
#[test]
fn builds_search_urls() {
let provider = provider();
assert_eq!(
provider.build_search_url("thai student", 1),
"https://www.thaiporntv.com/search/?q=thai%20student"
);
assert_eq!(
provider.build_search_url("thai student", 2),
"https://www.thaiporntv.com/search/?q=thai%20student&page=2"
);
}
#[test] #[test]
fn builds_tag_urls() { fn builds_tag_urls() {
let provider = provider(); let provider = provider();
@@ -720,7 +714,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
provider.build_tag_url("thai-massage", 2), provider.build_tag_url("thai-massage", 2),
"https://www.thaiporntv.com/tags/thai-massage/page/2/" "https://www.thaiporntv.com/tags/thai%2Dmassage/page/2/"
); );
} }
@@ -730,8 +724,17 @@ mod tests {
let formats = ThaipornTvProvider::decode_data_enc(encoded).unwrap(); let formats = ThaipornTvProvider::decode_data_enc(encoded).unwrap();
assert_eq!(formats.len(), 1); assert_eq!(formats.len(), 1);
assert_eq!(formats[0].url, "https://web.techvids.top/m3u8/1658_480p.m3u8"); assert_eq!(formats[0].url, "https://web.techvids.top/m3u8/1658_480p.m3u8");
assert_eq!(formats[0].quality, "480p"); // Verify format fields via JSON serialization (quality and http_headers are private)
assert_eq!(formats[0].http_headers.get("Referer").unwrap(), "https://www.thaiporntv.com"); let json = serde_json::to_value(&formats[0]).unwrap();
assert_eq!(json["quality"], "480p");
assert_eq!(json["http_headers"]["Referer"], BASE_URL);
}
#[test]
fn parses_duration() {
assert_eq!(parse_duration_mm_ss("50:47"), 3047);
assert_eq!(parse_duration_mm_ss("1:05:30"), 3930);
assert_eq!(parse_duration_mm_ss("12:04"), 724);
} }
#[tokio::test] #[tokio::test]
@@ -753,32 +756,7 @@ mod tests {
sort: Some("new".to_string()), sort: Some("new".to_string()),
sexuality: None, sexuality: None,
}; };
let videos = provider.get(VideoCache::new(), 1, "new", 10, options).await.unwrap(); let videos = provider.get(VideoCache::new(), 1, "new", None, 10, options).await.unwrap();
assert!(!videos.is_empty()); assert!(!videos.is_empty());
// Further assertions on video content
}
#[tokio::test]
#[ignore]
async fn fetches_and_parses_search() {
let provider = provider();
let options = ServerOptions {
featured: None,
category: None,
sites: None,
filter: None,
language: None,
public_url_base: Some("http://127.0.0.1:18080".to_string()),
requester: Some(Requester::new()),
network: None,
stars: None,
categories: None,
duration: None,
sort: Some("new".to_string()),
sexuality: None,
};
let videos = provider.query(VideoCache::new(), 1, "thai student", 10, options).await.unwrap();
assert!(!videos.is_empty());
// Further assertions on video content
} }
} }

View File

@@ -1,3 +1,4 @@
use base64::{engine::general_purpose, Engine};
use ntex::web; use ntex::web;
use crate::util::requester::Requester; use crate::util::requester::Requester;
use crate::videos::VideoFormat; use crate::videos::VideoFormat;
@@ -16,9 +17,10 @@ impl ThaipornTvProxy {
fn decode_data_enc(encoded_data: &str) -> Option<Vec<VideoFormat>> { fn decode_data_enc(encoded_data: &str) -> Option<Vec<VideoFormat>> {
let cleaned_data = encoded_data.replace("-", "+").replace("_", "/"); let cleaned_data = encoded_data.replace("-", "+").replace("_", "/");
let padded_data = format!("{:<pad$}", cleaned_data, pad = (cleaned_data.len() + 3) & !3); let padding = (4 - cleaned_data.len() % 4) % 4;
let padded_data = format!("{}{}", cleaned_data, "=".repeat(padding));
let decoded_bytes = match base64::decode(&padded_data) { let decoded_bytes = match general_purpose::STANDARD.decode(&padded_data) {
Ok(bytes) => bytes, Ok(bytes) => bytes,
Err(e) => { Err(e) => {
report_provider_error_background(CHANNEL_ID, "proxy.decode_data_enc.base64", &format!("error={e}")); report_provider_error_background(CHANNEL_ID, "proxy.decode_data_enc.base64", &format!("error={e}"));

View File

@@ -131,7 +131,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route(web::get().to(crate::proxies::pornhubthumb::get_image)), .route(web::get().to(crate::proxies::pornhubthumb::get_image)),
); );
cfg.service( cfg.service(
web::resource("/proxy/thaiporntv/{tail:.*}") web::resource("/thaiporntv/{endpoint}*")
.route(web::post().to(proxy2redirect)) .route(web::post().to(proxy2redirect))
.route(web::get().to(proxy2redirect)), .route(web::get().to(proxy2redirect)),
); );