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>
80 lines
2.8 KiB
Python
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("&", "&")
|
|
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
|