diff --git a/app/extractors/__init__.py b/app/extractors/__init__.py index 07f4abb..35806fe 100644 --- a/app/extractors/__init__.py +++ b/app/extractors/__init__.py @@ -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, diff --git a/app/extractors/tubes/fourk69.py b/app/extractors/tubes/fourk69.py index e56ac0f..c49809b 100644 --- a/app/extractors/tubes/fourk69.py +++ b/app/extractors/tubes/fourk69.py @@ -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 `` 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": "", "label": ""` 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":"","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("&", "&") + 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