goon/app/extractors/tubes/hentaimama.py
goon-foss 45fbe0412e fix: hentaimama i xanime graja, filmy mangoporn z hosterami, ostatni link filmu chroniony
hentaimama: nowy zapis id posta i 5 playerow w admin-ajax, adresy w JSON-ie;
bierzemy mp4 z gdvid (3 jakosci, bez tokenu), POST najpierw bez proxy.
Ogon "Watch Online Free in HD" w tytulach ucinany.
xanimeporn: przekierowanie download.php rozwiazywane na serwerze, apka
dostawala .php, otwierala WebView i pobierala plik zamiast grac.
Blad resolve (extractor pusty, HTTP strony) trafia do playback_events.

Filmy: tytul z okruszkow (motyw dooplay losuje "Watch"/"Now Playing:"),
linki nitroflare/rapidgator nie sa juz brane za wideo, sam landing idzie
do WebView, a przytrzymanie w apce nie zabija ostatniego linku filmu,
jesli serwer widzi, ze zyje. Skrypty naprawcze: tytuly dooplay,
tytuly i miniatury hentaimama, repair_dooplay_movies dla streampornvip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:12:35 +02:00

142 lines
5.9 KiB
Python

"""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=<id>`)
→ 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 `&amp;#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/<jakosc>/...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=<epoch>`. 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'<iframe[^>]+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