goon/app/extractors/tubes/fourk69.py
jtrzupek 81d617efc2 fix(extractors): 4k69 direct okcdn extraction (replaces WebView fallback)
Reverse-engineered the migrated 4k69 player: jwplayer now serves OK.ru CDN (okcdn.ru)
mp4s. The static page (SSR behind Cloudflare, fetched via proxy) carries "file"+"label"
pairs for every quality. okcdn's srcIp param is NOT enforced (cross-IP test 2026-06-14:
206 video/mp4 from a residential IP != srcIp), so the URL plays from any IP. Parse the
okcdn sources server-side and return them mobile_direct_ok — the phone plays the direct
video, no WebView, no VAST preroll, no age-gate, zero VPS proxy. Skips 4K/2K. Reverts
the brief _vps_blocked_fallback routing (WebView grabbed the preroll ad, not content).
Verified on emulator: native player streams the actual scene (report 5de3fbc5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 11:39:36 +02:00

68 lines
2.5 KiB
Python

"""4k69.com — okcdn.ru (OK.ru CDN) direct stream extractor.
2026-06-14: 4k69 zmigrowało player z get_file (4kporno.xxx) na jwplayer + okcdn.ru
(OK.ru video CDN). Strona (SSR za Cloudflare → curl_cffi/proxy) ma w inline jwplayer
setupie pary `"file": "<okcdn url>", "label": "<jakość>"` na WSZYSTKIE jakości
(4K/2K/1080p/720p/480p/360p/240p). To samo w LD-JSON `contentUrl` (jeden, niższy).
okcdn URL ma `expires=` (time-bound), `srcIp=` (IP edge Cloudflare który frontował
fetch) i `sig=` per jakość. KLUCZOWE (reverse-engineer + cross-IP test 2026-06-14):
`srcIp` NIE jest egzekwowane — URL gra z dowolnego IP (206 video/mp4 z residential IP
≠ srcIp). Więc resolwujemy server-side i oddajemy `mobile_direct_ok` → telefon gra
DIRECT, zero VPS proxy, zero WebView/reklam (VAST preroll jest runtime-only, nie ma go
w statycznym HTML, więc parsując HTML omijamy go całkiem).
Pomijamy 4K/2K (jak wcześniej 2160/1440 — za duże na mobile).
"""
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__)
# Pary file+label z jwplayer setupu: "file":"<okcdn>","label":"1080p". Label bierzemy
# wprost ze strony (pewniejsze niż mapowanie OK.ru type=N).
_OKCDN_FILE_RE = re.compile(
r'"file"\s*:\s*"(https?://[^"]*okcdn[^"]+)"\s*,\s*"label"\s*:\s*"([^"]+)"',
re.IGNORECASE,
)
# Za duże na mobile (jak stary skip 2160/1440).
_SKIP_LABEL_RE = re.compile(r"^(4k|2k|2160|1440)", re.IGNORECASE)
def _quality_num(label: str) -> int:
m = re.match(r"(\d+)", label or "")
return int(m.group(1)) if m else 0
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html = fetch_tube_html(page_url, timeout=timeout)
seen: set[str] = set()
out: list[StreamSource] = []
for m in _OKCDN_FILE_RE.finditer(html):
url = m.group(1).replace("&amp;", "&")
label = m.group(2).strip()
if url in seen:
continue
seen.add(url)
if _SKIP_LABEL_RE.match(label):
continue
out.append(StreamSource(
link=url,
quality=label,
type="mp4",
referer="https://4k69.com/",
# srcIp nieegzekwowane (cross-IP test 2026-06-14) → telefon gra direct.
raw={"mobile_direct_ok": True},
))
if not out:
log.info("4k69: no okcdn sources on %s", page_url)
return None
out.sort(key=lambda s: _quality_num(s.quality or ""), reverse=True)
return out