fix(porntrex): resolve get_file 302 backend-side, return portable CDN url
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) <noreply@anthropic.com>
This commit is contained in:
parent
a3be78373f
commit
bb4a17ae79
1 changed files with 75 additions and 9 deletions
|
|
@ -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/<srv>/<token>/<path>.mp4/` URL.
|
(480p / 720p / 1080p), każdy to `get_file/<srv>/<token>/<path>.mp4/` URL.
|
||||||
|
|
||||||
`get_file` 302 → `cdn.pcdn.cloudswitches.com/...mp4?expires=<ts>&md5=<sig>` — to
|
`get_file` 302 → `cdn.pcdn.cloudswitches.com/...mp4?expires=<ts>&md5=<sig>` — to
|
||||||
**time-bound signed URL** (nie IP-bound). Mobile ExoPlayer może pobrać get_file
|
**time-bound signed URL** (nie IP-bound, NIE cookie-bound) → po rozwiązaniu jest
|
||||||
(follow 302) i grać direct z CDN — zero VPS bandwidth. Stąd `mobile_direct_ok=True`.
|
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
|
REVISION 2026-05-31 (bug usera "porntrex wolno + brak wyboru jakości + chyba proxy"):
|
||||||
backendzie — oddajemy get_file URL, mobile zużywa token sam przy pierwszym fetchu.
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
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
|
from app.extractors._models import StreamSource
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -45,7 +56,55 @@ def _quality_rank(label: str | None) -> int:
|
||||||
return int(m.group(1)) if m else -1
|
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:
|
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
|
||||||
|
# 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)
|
html = fetch_tube_html(page_url, timeout=timeout)
|
||||||
|
|
||||||
# Mapa <var_name> → quality label (np. video_alt_url → "720p HD").
|
# Mapa <var_name> → quality label (np. video_alt_url → "720p HD").
|
||||||
|
|
@ -62,14 +121,21 @@ def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | Non
|
||||||
continue
|
continue
|
||||||
seen.add(url)
|
seen.add(url)
|
||||||
quality = quality_by_var.get(var_name)
|
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(
|
result.append(
|
||||||
StreamSource(
|
StreamSource(
|
||||||
link=url,
|
link=final_link,
|
||||||
type="mp4",
|
type="mp4",
|
||||||
quality=quality or None,
|
quality=quality or None,
|
||||||
referer=_BASE + "/",
|
referer=_BASE + "/",
|
||||||
# CDN po 302 jest time-bound (expires+md5), nie IP-bound —
|
# Finalny CDN url (cloudswitches) jest time-bound (expires+md5), nie
|
||||||
# mobile gra direct z get_file, zero VPS proxy bandwidth.
|
# cookie/IP-bound → mobile gra direct, zero VPS proxy bandwidth.
|
||||||
raw={"mobile_direct_ok": True},
|
raw={"mobile_direct_ok": True},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue