"""pornhat.com — KVS engine. get_file 302 → HLS m3u8 manifest. **2026-05-18 bandwidth optimization**: pornhat CDN tokens (`cdn.privatehost.com`) są **time-bound, nie IP-bound** (`?sign=&exp_time=`). Zweryfikowane Chrome DevTools MCP — VPS-resolved URL działa z każdego IP, bez Referer header. Zamiast zwracać `pornhat.com/get_file/` URL (mobile dostaje go i robi 302 chain przez VPS proxy), robimy server-side resolve i zwracamy końcowy manifest URL z signed token. Mobile ExoPlayer otrzymuje: `https://nvms12.cdn.privatehost.com/hls/contents/.../?sign=...&exp_time=...` i pobiera manifest + segments direct z CDN. **Zero VPS bandwidth** (poza ~5KB initial resolve fetch). `mobile_direct_ok=True` w `raw` mówi playback.py że dla type=m3u8 ten URL jest OK dla `direct_url=raw_url` (zazwyczaj m3u8 by szły przez proxy). Token wygasa za ~30-120 min od resolve (depends na lra param). User pause+resume po >2h może dostać 403 → mobile fallback na proxified URL re-resolve'a. """ from __future__ import annotations import logging import httpx from app.extractors._models import StreamSource from app.extractors.tubes._kvs_source import extract_kvs_sources log = logging.getLogger(__name__) def _resolve_get_file_redirect(get_file_url: str, *, timeout: float = 15.0) -> str | None: """Follow 302 chain pornhat.com/get_file/ → cdn.privatehost.com/hls/... Returns final manifest URL z signed token, lub None gdy fail. """ try: with httpx.Client( timeout=timeout, follow_redirects=True, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.pornhat.com/", }, ) as c: r = c.head(get_file_url) final = str(r.url) if "cdn.privatehost.com" in final and ".m3u8" not in final: # Generic master URL: /hls/contents/... CDN serves jako m3u8 mime # nawet bez .m3u8 w path (sprawdzone Content-Type). return final if ".m3u8" in final: return final log.info("pornhat resolve: unexpected final URL %s", final) return None except Exception as e: log.warning("pornhat resolve %s failed: %s", get_file_url, e) return None def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: sources = extract_kvs_sources( page_url, stream_type="m3u8", timeout=timeout, log_tag="pornhat" ) if not sources: return None # Resolve każdy get_file URL → CDN signed manifest URL. Mobile dostaje direct. resolved: list[StreamSource] = [] for s in sources: final = _resolve_get_file_redirect(s.link) if final: resolved.append( StreamSource( link=final, type="m3u8", quality=s.quality, referer=s.referer, raw={"mobile_direct_ok": True}, ) ) else: # Fallback: keep original (proxy will re-resolve) resolved.append(s) return resolved