goon/app/extractors/tubes/kporner8.py
goon-foss f5b7a45ac0 feat(8kporner): browse scraper za obejściem CF + extractor JWPlayer sources
Sitemapa i /watch/*.html są za Cloudflare, paginacja zwraca stronę 1, a load-more
chce logowania, więc katalog buduje się z homepage (~115 scen), a detale lecą przez
?link1=watch&id=. Bramka na obsadę przepuszcza 62/115; z tego 97% performerów ma
ref tpdb/stashdb, a 60% scen dopina się do tego, co już mamy.

Extractor czyta tablicę sources JWPlayera, nie JSON-LD contentUrl (ten jest zawsze
144p). Cap 720p, bo okcdn dławi połączenie do ~2,1 Mbps. Stream przenośny cross-IP
(206 bez Referera, faststart) → mobile_direct_ok.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:15:32 +02:00

80 lines
2.8 KiB
Python

"""8kporner.com — JWPlayer sources → direct mp4 na OK.ru CDN. Dodany 2026-07-27.
**Pułapka, o którą łatwo się rozbić:** JSON-LD `contentUrl` na tej stronie to ZAWSZE
144p (256x144, sprawdzone). Prawdziwe jakości siedzą wyłącznie w tablicy `sources`
JWPlayera. Skopiowanie wzorca z `hqfap.py` (który czyta `contentUrl`) wysłałoby
każdemu userowi 144p.
Fetch idzie przez `?link1=watch&id=<id>`, bo kanoniczna forma `/watch/<slug>_<id>.html`
jest CF-blokowana (403, również przez proxy).
CDN to `vd*.okcdn.ru` / `ok6-*.vkuser.net` — ta sama rodzina co 4k69/hqfap: `srcIp`
jest w URL-u, ale NIE jest egzekwowane, więc token działa cross-IP (mobile_direct).
**Limit przepustowości:** OK.ru dławi pojedyncze połączenie do ~2,1 Mbps. 1080p
potrzebuje 3,8 Mbps, 2K 6,2, a 4K 13,8 — czyli nie dociągną się w czasie rzeczywistym
i dałyby „loading forever" (ten sam objaw co przy 4K na fullmovies). Dlatego oddajemy
maksymalnie 720p (1,5 Mbps), zgodnie z decyzją podjętą już dla 4k69.
"""
from __future__ import annotations
import logging
import re
from app.extractors._fetch import fetch_tube_html
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_BASE = "https://8kporner.com"
_SCENE_ID_RE = re.compile(r"_(\d+)\.html")
# Pary z JWPlayer setupu: "file":"<url>","label":"720p"
_SOURCE_RE = re.compile(
r'"file"\s*:\s*"(https?://[^"]+)"\s*,\s*"label"\s*:\s*"([^"]+)"', re.IGNORECASE
)
# Powyżej 720p CDN nie nadąża (patrz docstring).
_MAX_HEIGHT = 720
def _height(label: str) -> int:
lab = (label or "").strip().lower()
if lab.startswith("4k"):
return 2160
if lab.startswith("2k"):
return 1440
m = re.match(r"(\d{3,4})", lab)
return int(m.group(1)) if m else 0
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
# Kanoniczny URL → forma omijająca CF.
m = _SCENE_ID_RE.search(page_url)
fetch_url = f"{_BASE}/?link1=watch&id={m.group(1)}" if m else page_url
html_text = fetch_tube_html(fetch_url, timeout=timeout)
seen: set[str] = set()
out: list[StreamSource] = []
for sm in _SOURCE_RE.finditer(html_text):
url = sm.group(1).replace("&amp;", "&")
label = sm.group(2).strip()
h = _height(label)
if url in seen or h == 0 or h > _MAX_HEIGHT:
continue
seen.add(url)
out.append(
StreamSource(
link=url,
type="mp4",
quality=label,
referer=_BASE + "/",
# srcIp nieegzekwowane (jak 4k69) → telefon gra direct z CDN.
raw={"mobile_direct_ok": True},
)
)
if not out:
log.info("8kporner: brak sources <=720p na %s", page_url)
return None
out.sort(key=lambda s: _height(s.quality or ""), reverse=True)
return out