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>
119 lines
4.4 KiB
Python
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
|