From bb4a17ae79cd2eb7915739dc8bbea3ec18c20586 Mon Sep 17 00:00:00 2001 From: jtrzupek Date: Sun, 31 May 2026 21:47:26 +0200 Subject: [PATCH] fix(porntrex): resolve get_file 302 backend-side, return portable CDN url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User bug: porntrex plays slowly, no quality picker, reload flicker — suspected VPS proxy. Root cause: porntrex KVS get_file tokens are cookie/session-bound, not just time-bound as previously assumed. The extractor handed mobile the raw get_file url; ExoPlayer's cookieless request → 410 → mobile fell back to the VPS proxy (slow + nav.replace flicker). Verified: following get_file in the same curl_cffi session that fetched the page → 200 (streams video); a fresh session → 410. The final CDN url after the 302 (cdn.pcdn.cloudswitches.com/...?expires=&md5=) is portable — fresh session → 206. Fix: extract() now uses one curl_cffi Session for page + get_file, follows each quality's 302 (stream + Range, no body download) and returns the resolved CDN url. Mobile plays direct, multi-quality picker works, zero proxy bandwidth. Falls back to the raw get_file url if a resolve fails. Verified on prod: both 720p/480p now resolve to cloudswitches CDN. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/extractors/tubes/porntrex.py | 84 ++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/app/extractors/tubes/porntrex.py b/app/extractors/tubes/porntrex.py index 85a48a5..37aaa7a 100644 --- a/app/extractors/tubes/porntrex.py +++ b/app/extractors/tubes/porntrex.py @@ -7,18 +7,29 @@ KVS player: detail page ma `flashvars` z `video_url` / `video_alt_url` / `video_ (480p / 720p / 1080p), każdy to `get_file///.mp4/` URL. `get_file` 302 → `cdn.pcdn.cloudswitches.com/...mp4?expires=&md5=` — to -**time-bound signed URL** (nie IP-bound). Mobile ExoPlayer może pobrać get_file -(follow 302) i grać direct z CDN — zero VPS bandwidth. Stąd `mobile_direct_ok=True`. +**time-bound signed URL** (nie IP-bound, NIE cookie-bound) → po rozwiązaniu jest +portable: mobile gra direct z CDN, zero VPS bandwidth. -Token w `get_file` URL bywa single-use (410 po reuse), więc NIE resolvujemy 302 na -backendzie — oddajemy get_file URL, mobile zużywa token sam przy pierwszym fetchu. +REVISION 2026-05-31 (bug usera "porntrex wolno + brak wyboru jakości + chyba proxy"): +Wcześniejsze założenie "mobile zużyje get_file sam" było BŁĘDNE — `get_file` token jest +**cookie/session-bound**: działa tylko w tej samej sesji curl_cffi która pobrała stronę. +Osobny request mobile (ExoPlayer, bez cookies) → 410 → mobile spadał na VPS proxy +(stąd flicker = nav.replace + wolne odtwarzanie). Zweryfikowane: same-session follow +get_file → 200 (streamuje wideo); fresh session → 410. Finalny CDN url (cloudswitches, +expires+md5) jest natomiast portable (fresh session → 206). + +FIX: resolvujemy 302 NA BACKENDZIE (w tej samej sesji co fetch strony) i oddajemy +**finalny CDN url** per jakość. Mobile gra direct, multi-quality picker działa, zero proxy. +Token get_file zużywamy raz tu; CDN url jest time-bound (nie single-use) → starcza na +sesję odtwarzania. """ from __future__ import annotations import logging import re +import time -from app.extractors._fetch import fetch_tube_html +from app.extractors._fetch import _DEFAULT_IMPERSONATE, _DEFAULT_UA, _HAS_CURL_CFFI, fetch_tube_html from app.extractors._models import StreamSource log = logging.getLogger(__name__) @@ -45,8 +56,56 @@ def _quality_rank(label: str | None) -> int: return int(m.group(1)) if m else -1 +def _resolve_get_file(session, get_file_url: str, timeout: float) -> str | None: + """Follow get_file 302 → finalny portable CDN url (w sesji która ma cookies strony). + + `?rnd=` cache-bust jak kt_player. stream=True + Range → łapiemy tylko nagłówki/finalny + URL po redirectach, NIE pobieramy 644MB body. Zwraca None gdy resolve padł.""" + sep = "&" if "?" in get_file_url else "?" + url = f"{get_file_url}{sep}rnd={int(time.time() * 1000)}" + try: + r = session.get( + url, + timeout=timeout, + allow_redirects=True, + stream=True, + headers={"Referer": _BASE + "/", "Range": "bytes=0-1"}, + ) + final = str(r.url) + status = r.status_code + r.close() + except Exception as e: + log.info("porntrex: get_file resolve failed (%s): %s", get_file_url[:60], e) + return None + if status >= 400 or "/get_file/" in final: + log.info("porntrex: get_file resolve bad status=%s final=%s", status, final[:70]) + return None + return final + + def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: - html = fetch_tube_html(page_url, timeout=timeout) + # Wspólna sesja: get_file token jest cookie/session-bound, więc 302 MUSI być + # rozwiązany w tej samej sesji curl_cffi co fetch strony (patrz docstring). + session = None + if _HAS_CURL_CFFI: + from curl_cffi import requests as _cf_requests + session = _cf_requests.Session(impersonate=_DEFAULT_IMPERSONATE) + try: + resp = session.get( + page_url, + headers={"User-Agent": _DEFAULT_UA, "Accept": "text/html,application/xhtml+xml"}, + timeout=timeout, + allow_redirects=True, + ) + html = resp.text if resp.status_code < 400 else "" + except Exception as e: + log.info("porntrex: page fetch failed %s: %s", page_url, e) + html = "" + if not html: + html = fetch_tube_html(page_url, timeout=timeout) + session = None # fetch_tube_html użył innej sesji → nie resolvuj w `session` + else: + html = fetch_tube_html(page_url, timeout=timeout) # Mapa → quality label (np. video_alt_url → "720p HD"). quality_by_var: dict[str, str] = {} @@ -62,14 +121,21 @@ def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | Non continue seen.add(url) quality = quality_by_var.get(var_name) + # Rozwiąż get_file → portable CDN url (w sesji ze stroną). Gdy resolve padnie, + # oddaj get_file jako fallback (mobile spróbuje direct → ewentualnie proxy). + final_link = url + if session is not None: + resolved = _resolve_get_file(session, url, timeout) + if resolved: + final_link = resolved result.append( StreamSource( - link=url, + link=final_link, type="mp4", quality=quality or None, referer=_BASE + "/", - # CDN po 302 jest time-bound (expires+md5), nie IP-bound — - # mobile gra direct z get_file, zero VPS proxy bandwidth. + # Finalny CDN url (cloudswitches) jest time-bound (expires+md5), nie + # cookie/IP-bound → mobile gra direct, zero VPS proxy bandwidth. raw={"mobile_direct_ok": True}, ) )