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>
This commit is contained in:
jtrzupek 2026-06-14 11:39:36 +02:00
parent 2a9445fe4a
commit 81d617efc2
2 changed files with 40 additions and 30 deletions

View file

@ -181,14 +181,12 @@ _REGISTRY: dict[str, Callable[[str], list[StreamSource] | None]] = {
# Cross-IP test 2026-06-10: oba CDN-y portable (`ip=`/`srcIp=` nie egzekwowane),
# tokeny time-bound → on-demand fetch daje świeży URL. Mobile direct, zero proxy.
"hqfapcom": hqfap.extract,
# 4k69 — 2026-06-14 PRZEPIĘTE na _vps_blocked_fallback (WebView). Strona zmigrowała
# player z get_file (4kporno.xxx) na jwplayer + okcdn.ru z `srcIp=` w tokenie =
# IP-bound; plus 4k69 jest za Cloudflare (VPS fetch tylko przez proxy). Native
# extractor (get_file regex) zwracał None → "host problem" (zgłoszenie 5de3fbc5).
# WebView na telefonie: residential IP przechodzi CF, okcdn token bound do IP
# telefonu, INJECTED_JS łapie jwplayer video.src → ExoPlayer gra. fourk69.extract
# zostaje w module gdyby strona wróciła do get_file.
"4k69com": _vps_blocked_fallback.extract,
# 4k69 — 2026-06-14 player zmigrowany na jwplayer + okcdn.ru (OK.ru CDN). Natywny
# fourk69.extract parsuje okcdn `file`+`label` ze strony (SSR za CF → proxy). okcdn
# srcIp NIE egzekwowane (cross-IP test) → mobile_direct_ok, telefon gra direct.
# Pełny reverse-engineer w fourk69.py (zgłoszenie 5de3fbc5). [Krótko był na
# _vps_blocked_fallback/WebView, ale to łapało VAST preroll zamiast contentu.]
"4k69com": fourk69.extract,
# neporn — KVS function/0 + license (jak freshporno). Server-side _kvs resolve →
# data001.neporn.com/remote_control.php portable (cross-IP 206, 2026-06-10).
"neporncom": neporn.extract,

View file

@ -1,13 +1,18 @@
"""4k69.com — get_file stream extractor (platforma jak fullmovies/hdporngg).
"""4k69.com — okcdn.ru (OK.ru CDN) direct stream extractor.
Scene page (SSR za Cloudflare curl_cffi) ma 3 get_file URL-e na www.4kporno.xxx
(`..._2160m.mp4` / `_720m` / `_480m`) w JSON-LD contentUrl i w JS playera, NIE
w `<source>` tagach (dlatego nie _source_getfile, tylko skan całej strony).
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).
Jak fpvcdn (fullmovies, ta sama rodzina `/get_file/8512/`): get_file binduje CDN
do IP fetchera, jest stateless i ważny 90s oddajemy NIEZRESOLWOWANE z
mobile_direct_ok telefon follow-uje 302 z własnym IP (cross-IP test 2026-06-10:
lokalny ISP 206 video/mp4). 2160p pomijamy (CDN time-out ~30s, jak fpvcdn).
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
@ -19,9 +24,19 @@ from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_GET_FILE_RE = re.compile(r"https://[a-z0-9.\-]+/get_file/[^\s\"'\\]+\.mp4/?", re.IGNORECASE)
_QUALITY_RE = re.compile(r"_(\d{3,4})[mp]?\.mp4", re.IGNORECASE)
_SKIP_QUALITY_RE = re.compile(r"^(2160|1440)$")
# 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:
@ -29,28 +44,25 @@ def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | Non
seen: set[str] = set()
out: list[StreamSource] = []
for m in _GET_FILE_RE.finditer(html):
url = m.group(0)
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)
qm = _QUALITY_RE.search(url)
quality_num = qm.group(1) if qm else None
if quality_num and _SKIP_QUALITY_RE.match(quality_num):
continue
# `_preview.mp4` itp. bez liczby jakości — pomiń (trailer, nie scena).
if not quality_num:
if _SKIP_LABEL_RE.match(label):
continue
out.append(StreamSource(
link=url,
quality=f"{quality_num}p",
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 get_file URLs on %s", page_url)
log.info("4k69: no okcdn sources on %s", page_url)
return None
out.sort(key=lambda s: int((s.quality or "0p")[:-1]), reverse=True)
out.sort(key=lambda s: _quality_num(s.quality or ""), reverse=True)
return out