"""xanimeporn.com — mp4 z endpointu pobierania. Dodany 2026-08-04. Player to Fluid Player z **pustym ``** — ź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=&quality=1080p https://videos.xanimeporn.com/download.php?id=&quality=720p `postId` siedzi w klasie `` (`postid-69951`), więc nie trzeba parsować zakładki — składamy URL sami dla obu jakości. **Oddajemy docelowy plik, nie `download.php` (zmiana 2026-10-15).** Endpoint przekierowuje na `download.xanimeporn.com/videos//...mp4?s=&e=` z `Content-Disposition: attachment`. Wczesniej oddawalismy `download.php`, zeby telefon sam bral swiezy podpis, ale apka po `.php` w adresie uznawala link za strone playera i otwierala go w WebView, ktory POBIERAL plik do Pobranych zamiast grac (zgloszenie 54b17aed; od dodania serwisu ani jedno odtworzenie). Podpis zyje ok. 2 h i nie jest zwiazany z IP (adres z VPS-a gra z domowego), a resolve dzieje sie przy kliknieciu, wiec rozwiazujemy przekierowanie tutaj. Gdy sie nie uda, zostaje `download.php` jako zapas. 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 import httpx 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") _UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36" 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) out: list[StreamSource] = [] for q in _QUALITIES: link = f"https://videos.xanimeporn.com/download.php?id={post_id}&quality={q}" try: res = httpx.get( link, headers={"User-Agent": _UA, "Referer": _BASE + "/"}, follow_redirects=False, timeout=15.0, ) location = res.headers.get("location") or "" if res.status_code in (301, 302, 303, 307, 308) and location.startswith("http"): link = location except Exception as e: log.info("xanimeporn: przekierowanie %s %s nie wyszlo: %s", post_id, q, e) out.append( StreamSource( link=link, type="mp4", quality=q, referer=_BASE + "/", # Zweryfikowane 206 + faststart, podpis nie jest zwiazany z IP. raw={"mobile_direct_ok": True}, ) ) return out