"""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. **2026-10-15 (zgloszenie 6ab3a973, zadna scena nie grala):** strona zmienila zapis na `{action: 'get_player_contents',a: '79669'}` (spacje), a odpowiedz admin-ajax ma 5 playerow (`?dt_embed=rtmp|hls|hls-mp4|r2-mp4` i `newjav.php`) z `&#038;` w src. Strony playerow trzymaja adresy w JSON-ie (ukosniki zapisane jako backslash i slash). javprovider daje 404 dla nowych odcinkow, gdvid (`/hls/output/.../mp4//...mp4`, bez tokenu) gra wszedzie. POST przechodzi dzis z VPS-a bez proxy, Bright Data zostaje tylko na wypadek 403/Cloudflare. **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 html 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:\s*['"]get_player_contents['"],\s*a:\s*['"](\d+)['"]""") _BODY_POST_ID_RE = re.compile(r"\bpostid-(\d+)\b") _IFRAME_SRC_RE = re.compile(r']+src="([^"]+)"', re.IGNORECASE) _VIDEO_RE = re.compile(r"""https?://[^\s"'<>]+\.(?:mp4|m3u8)[^\s"'<>]*""") _QUALITY_RE = re.compile(r"/mp4/(\d{3,4})p/") #: Host bez tokenu idzie pierwszy, jego URL nie wygasa. javprovider na koniec (404 na nowych). _HOST_RANK = ("gdvid.info/hls/", "gdvid", "hentaidoge", "javprovider") def _rank(url: str) -> int: for i, host in enumerate(_HOST_RANK): if host in url: return i return len(_HOST_RANK) def _quality(url: str) -> int: m = _QUALITY_RE.search(url) return int(m.group(1)) if m else 0 def _player_contents(page_url: str, post_id: str, timeout: float): """POST get_player_contents: najpierw bez proxy, Bright Data tylko przy blokadzie.""" from curl_cffi import requests as cr proxy = get_settings().brightdata_proxy_url attempts = [None] + ([proxy] if proxy else []) for px in attempts: session = cr.Session( impersonate="chrome120", proxies={"http": px, "https": px} if px else None, verify=False, ) try: # Strona odcinka najpierw, bez jej ciasteczek CF potrafi odrzucic POST-a. session.get(page_url, timeout=timeout) res = session.post( f"{_BASE}/wp-admin/admin-ajax.php", data={"action": "get_player_contents", "a": post_id}, headers={"Referer": page_url, "X-Requested-With": "XMLHttpRequest"}, timeout=timeout, ) if res.status_code == 200: return session, json.loads(res.text) log.info("hentaimama: admin-ajax %s (proxy=%s) dla %s", res.status_code, bool(px), page_url) except Exception as e: log.info("hentaimama: get_player_contents (proxy=%s) %s: %s", bool(px), page_url, e) return None, None def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: html_text = fetch_tube_html(page_url, timeout=timeout) pm = _POST_ID_RE.search(html_text) or _BODY_POST_ID_RE.search(html_text) if not pm: log.info("hentaimama: brak id posta na %s", page_url) return None session, fields = _player_contents(page_url, pm.group(1), timeout) if session is None: return None players: list[str] = [] for field in fields if isinstance(fields, list) else []: for src in _IFRAME_SRC_RE.findall(str(field)): src = html.unescape(html.unescape(src)) 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 body = body.replace(chr(92) + "/", "/") for link in _VIDEO_RE.findall(body): if link in seen or "thumbnail" in link or link.endswith(".vtt"): continue seen.add(link) q = _quality(link) out.append( StreamSource( link=link, type="m3u8" if ".m3u8" in link else "mp4", quality=f"{q}p" if q else None, referer=_BASE + "/", # Zweryfikowane: 206 bez Referera, faststart, telefon gra direct. raw={"mobile_direct_ok": True}, ) ) if not out: log.info("hentaimama: nie znalazlem wideo dla %s", page_url) return None out.sort(key=lambda s: (_rank(s.link), s.type != "mp4", -_quality(s.link))) return out