goon/app/extractors/tubes/pornhat.py
goon-foss ad0284585b Initial commit
Goon — self-hosted aggregator for adult-content scene metadata.

Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites.
Cross-source deduplication via perceptual hash + Levenshtein distance.
FastAPI backend + APScheduler worker + React Native (Expo) mobile client.

FOSS, ad-free, donation-funded. See README for details.
2026-05-20 10:10:22 +02:00

86 lines
3.2 KiB
Python

"""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=<HMAC>&exp_time=<unix>`). 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