"""hentaimama.io — bezpośredni mp4 przez trzystopniowe rozwiązanie playera. Dodany 2026-08-04. Strona odcinka NIE zawiera playera w HTML. Ścieżka wygląda tak: 1. GET strony odcinka → id posta z `action:'get_player_contents', a:'15377'` 2. POST `/wp-admin/admin-ajax.php` (`action=get_player_contents`, `a=`) → JSON z dwoma iframe'ami do WŁASNYCH playerów serwisu 3. GET strony playera (`new2.php` / `newjav.php`) → bezpośredni link mp4 Parametr `p` w URL-u playera to base64 ścieżki pliku (`D/deco-x-deco-animation-1.mp4`), ale nie trzeba go składać ręcznie — bierzemy iframe taki, jaki przyszedł. **Krok 2 wymaga Bright Data.** GET-y przechodzą z gołego IP, ale sam POST na `admin-ajax.php` dostaje challenge Cloudflare (403, „Just a moment..."), dokładnie jak `POST /api/video-info` przy sexu. Bez klucza proxy extractor zwraca None. **Preferujemy `gdvid.info` nad `javprovider.com`**: pierwszy oddaje plik BEZ tokenu, więc URL nigdy nie wygasa; drugi ma `?st=...&e=`. Oba zweryfikowane z VPS-a: 206 na Range, `ftypisom` w pierwszym KB (faststart), działają BEZ Referera — stąd `mobile_direct_ok`, telefon gra prosto z CDN. """ from __future__ import annotations import json import logging import re from app.config import get_settings from app.extractors._fetch import fetch_tube_html from app.extractors._models import StreamSource log = logging.getLogger(__name__) _BASE = "https://hentaimama.io" _POST_ID_RE = re.compile(r"action:'get_player_contents',\s*a:'(\d+)'") _IFRAME_SRC_RE = re.compile(r']+src="([^"]+)"', re.IGNORECASE) _MP4_RE = re.compile(r'https?://[^\s"\'<>]+\.mp4[^\s"\'<>]*') #: Host bez tokenu idzie pierwszy — jego URL nie wygasa. _HOST_RANK = ("gdvid", "javprovider") def _rank(url: str) -> int: for i, host in enumerate(_HOST_RANK): if host in url: return i return len(_HOST_RANK) def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: proxy = get_settings().brightdata_proxy_url if not proxy: log.info("hentaimama: brak BRIGHTDATA_PROXY_URL — CF zablokuje POST admin-ajax") return None html_text = fetch_tube_html(page_url, timeout=timeout) pm = _POST_ID_RE.search(html_text) if not pm: log.info("hentaimama: brak id posta na %s", page_url) return None from curl_cffi import requests as cr try: session = cr.Session( impersonate="chrome120", proxies={"http": proxy, "https": proxy}, verify=False, ) # Strona odcinka najpierw — bez jej ciasteczek CF odrzuca POST-a. session.get(page_url, timeout=timeout) res = session.post( f"{_BASE}/wp-admin/admin-ajax.php", data={"action": "get_player_contents", "a": pm.group(1)}, headers={"Referer": page_url, "X-Requested-With": "XMLHttpRequest"}, timeout=timeout, ) fields = json.loads(res.text) except Exception as e: log.info("hentaimama: get_player_contents failed %s: %s", page_url, e) return None players: list[str] = [] for field in fields if isinstance(fields, list) else []: for src in _IFRAME_SRC_RE.findall(str(field)): if src.startswith("//"): src = "https:" + src if src.startswith("http") and src not in players: players.append(src) out: list[StreamSource] = [] seen: set[str] = set() for player_url in players: try: body = session.get( player_url, headers={"Referer": page_url}, timeout=timeout ).text except Exception as e: log.info("hentaimama: player fetch failed %s: %s", player_url, e) continue for link in _MP4_RE.findall(body): if link in seen: continue seen.add(link) out.append( StreamSource( link=link, type="mp4", quality=None, # serwis nie podaje rozdzielczości referer=_BASE + "/", # Zweryfikowane: 206 bez Referera, faststart → telefon gra direct. raw={"mobile_direct_ok": True}, ) ) if not out: log.info("hentaimama: nie znalazłem mp4 dla %s", page_url) return None out.sort(key=lambda s: _rank(s.link)) return out