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.
86 lines
3.2 KiB
Python
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
|