goon/app/extractors/tubes/hentaimama.py
goon-foss 85ee42d5a3 feat(hentai): konektor hentaimama.io - pierwsze zrodlo wertykalu hentai
Jednostka tresci to ODCINEK (/episodes/<seria>-episode-N/), mapowany na scene tak samo
jak w wertykale JAV, bez zmian w schemacie. Seria ladue jako tag, wiec odcinki daja sie
grupowac.

KRYTERIUM JEST TU INNE I TO CELOWE. Dla animacji nie istnieje kanon - TPDB i StashDB
nie maja rysowanych postaci - wiec kazda scena stad jest technicznie orphanem i to
jest POPRAWNE. Nie stosujemy ani bramki na obsade (nie ma obsady), ani miary canonical
match, ktora sluzyla nam do oceny tubow z zywymi aktorami. Jakosc mierzymy kompletem
seria + odcinek + gatunki.

Motyw jest z rodziny Dooplay, ale DooplayConnector NIE pasuje: brak data-fl-source i
data-post, na ktorych stoi tamten parser. Player idzie w trzech krokach: id posta ze
strony odcinka, POST /wp-admin/admin-ajax.php z action=get_player_contents, potem
strona wlasnego playera, ktora oddaje bezposredni mp4. Sam POST jest za Cloudflare
(GET-y przechodza), wiec wymaga Bright Data, dokladnie jak POST /api/video-info w sexu.

Playback najlepszy z mozliwych: preferujemy gdvid.info, bo oddaje plik BEZ tokenu,
wiec URL nie wygasa. Zweryfikowane z VPS: 206 na Range, ftypisom (faststart), dziala
BEZ Referera, stad mobile_direct_ok.

Pulapka obeszla: /genre/ w calym HTML to 118 linkow (chmura w sidebarze, ten sam wzorzec
co przy sexu i yourdailypornmovies). Gatunki bierzemy tylko z div.sgeneros, co daje 5-10
realnych pozycji na odcinek.

Pilot: seen 90, new 90, errors 0. W bazie 90 odcinkow, 100 procent z data i tagami,
zakres 2025-12-29 do 2026-07-31.

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

119 lines
4.4 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.
**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 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'<iframe[^>]+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