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>
142 lines
5.9 KiB
Python
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 `&#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
|