goon/app/extractors/tubes/xanimeporn.py
goon-foss 7a8e10c175 feat(hentai): konektor xanimeporn.com - drugie zrodlo wertykalu
NIE jest re-hostem hentaimamy i to bylo pierwsze, co sprawdzilem. Wspolny odcinek
(Shoujo Ramune 7) lezy na obu stronach, ale to zupelnie inne pliki: hentaimama serwuje
gdvid.info/S/shoujo-ramune-7.mp4 w jednej jakosci bez etykiety, a tutaj jest
download.xanimeporn.com z jawnym wyborem 1080p (222 MB) i 720p (84 MB).

Listing idzie z RSS-a i to jedyna droga, ktora dziala. Sprawdzone trzy inne:
- /page/2/ i /page/3/ oddaja ten sam zestaw co strona 1 (28 z 32 wspolnych),
  czyli paginacja zepsuta jak przy 8kporner
- /uncensored/ i /censored/ stronicuja poprawnie, ale NIE sa chronologiczne: pierwsze
  pozycje z 2024, zakres 2015-2026. Browse-latest nigdy nie zobaczylby nowego odcinka,
  czyli pulapka z freshporno. Pierwsza wersja konektora wlasnie tak dzialala i dopiero
  test na danych to pokazal
- /hentai-list/ ma caly katalog (1048) na jednej stronie, ale nie jest chronologiczny

Drugi filtr tez wymuszony przez dane: slugi gatunkow (/futanari/, /big-tits/) sa takimi
samymi slugami najwyzszego poziomu jak odcinki i wchodzily jako pozycje bez daty i
miniaturki. _parse_detail wymaga teraz postid w klasie body, ktory maja tylko
pojedyncze wpisy. Ten sam identyfikator jest potrzebny extractorowi.

Playback: Fluid Player z PUSTYM <source src=''>, wideo wstrzykiwane w runtime, wiec w
HTML nie ma zadnego linku. Wyjsciem jest zakladka Downloads:
videos.xanimeporn.com/download.php?id=<postId>&quality=. Oddajemy download.php, NIE
docelowy plik, zeby token generowal sie przy odtwarzaniu i nie wietrzal (lekcja z
watchporn). Bez Cloudflare i bez proxy, w odroznieniu od hentaimamy.

Pilot: seen 50, new 50, errors 0. Metadane: 100 procent z data, miniaturka, kategoria
i seria jako tagi.

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

60 lines
2.4 KiB
Python

"""xanimeporn.com — mp4 z endpointu pobierania. Dodany 2026-08-04.
Player to Fluid Player z **pustym `<source src=''>`** — źródło wstrzykiwane jest w
runtime, a skrypt inicjalizujący obsługuje wyłącznie fullscreen i analitykę. W statycznym
HTML nie ma ani jednego linku do wideo, więc wygląda to na ślepy zaułek.
Wyjściem jest zakładka „Downloads", która niesie:
https://videos.xanimeporn.com/download.php?id=<postId>&quality=1080p
https://videos.xanimeporn.com/download.php?id=<postId>&quality=720p
`postId` siedzi w klasie `<body>` (`postid-69951`), więc nie trzeba parsować zakładki —
składamy URL sami dla obu jakości.
**Oddajemy `download.php`, a NIE docelowego pliku.** Endpoint przekierowuje na
`download.xanimeporn.com/...?s=<sig>&e=<epoch>`, czyli URL z tokenem i wygasaniem.
Zwracając `download.php` zostawiamy generowanie tokenu na moment odtwarzania: telefon
idzie za przekierowaniem i dostaje świeży podpis, więc nic nie wietrzeje. To ta sama
lekcja co przy watchporn, gdzie zapisany statyczny URL po czasie padał.
Zweryfikowane z VPS-a: 1080p 222 MB i 720p 84 MB, oba 206 na Range z `ftypisom`/`ftypmp42`
w pierwszym KB (faststart). Wymaga Referera i UA, oba dokłada `playback.py`. Bez
Cloudflare i bez proxy, w odróżnieniu od hentaimamy.
"""
from __future__ import annotations
import logging
import re
from app.extractors._fetch import fetch_tube_html
from app.extractors._models import StreamSource
log = logging.getLogger(__name__)
_BASE = "https://xanimeporn.com"
_POST_ID_RE = re.compile(r"\bpostid-(\d+)\b")
#: Serwis publikuje dokładnie te dwie jakości; kolejność = preferencja.
_QUALITIES = ("1080p", "720p")
def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None:
html_text = fetch_tube_html(page_url, timeout=timeout)
m = _POST_ID_RE.search(html_text)
if not m:
log.info("xanimeporn: brak postid w klasie body na %s", page_url)
return None
post_id = m.group(1)
return [
StreamSource(
link=f"https://videos.xanimeporn.com/download.php?id={post_id}&quality={q}",
type="mp4",
quality=q,
referer=_BASE + "/",
# Zweryfikowane 206 + faststart z VPS-a; telefon sam podąża za 302 i
# dostaje własny, świeży token.
raw={"mobile_direct_ok": True},
)
for q in _QUALITIES
]