"""perverzija.com (tube.perverzija.com), natywny HLS zamiast WebView. Ustalone trace'em CDP (2026-07-28, bug-reports 40ec88cc / 92e9133e "zacina się na pierwszych sekundach i robi loop"): Strona osadza iframe `pervl4.xtremestream.xyz/player/index.php?data=`. Samo źródło tego iframe'a NIE zawiera streamu (jest tam wyłącznie preroll/midroll z opencdn.b-cdn.net to one leciały w pętli). Player dociąga wideo dopiero w runtime: GET /player/xs1.php?data=&q=<480|720> → GOTOWY manifest HLS (#EXTM3U, VOD, segmenty po ~4,17 s z PEŁNYMI URL-ami do pervl4.xspcdn{01..10}.sa.com/cdn/down/...) Segmenty mają rozszerzenie `.html` (kamuflaż), ale to zwykły HLS, nic nie trzeba deszyfrować ani składać. Wystarczy oddać URL `xs1.php` jako `type='m3u8'`. WAŻNE (mobile): ścieżka manifestu NIE kończy się na `.m3u8`, a expo-video bez contentType potraktowałby ją jako progressive i padł, dokładnie casus pornhat. Dlatego manifest musi iść przez `/proxy/hls` (passthrough); segmenty i tak lecą direct z CDN, więc przez VPS przechodzi ~41 KB, nie wideo. `referer` celowo wskazuje na `index.php?data=`, taki Referer wysyła przeglądarka przy wywołaniu xs1.php i bez niego jest ryzyko 403. """ from __future__ import annotations import logging import re from app.extractors._models import StreamSource from app.extractors import browser_get log = logging.getLogger(__name__) # iframe playera: https://pervl4.xtremestream.xyz/player/index.php?data=<32 hex> _PLAYER_IFRAME_RE = re.compile( r"https?://(?P[a-z0-9\-]+\.xtremestream\.[a-z]+)/player/index\.php\?data=(?P[0-9a-f]{16,64})", re.IGNORECASE, ) # Player wystawia te dwie jakości (trace: xs1.php?...&q=480 i &q=720). Kolejność = # preferencja (najlepsza pierwsza), tak jak w pozostałych ekstraktorach. _QUALITIES = ("720", "480") def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: try: res = browser_get(page_url, timeout=timeout) html = res.text if hasattr(res, "text") else str(res) except Exception as e: log.info("perverzija page fetch failed %s: %s", page_url, e) return None m = _PLAYER_IFRAME_RE.search(html) if not m: # ~10% katalogu siedzi na innym playerze niż xtremestream (próbka 30 stron # 2026-08-20: 27 xtremestream, 1 mixdrop, 1 playhydrax, 1 nie do pobrania). # Oddajemy je generycznemu embed-iframe: wychodzi type='hoster', czyli telefon # resolwuje je po swojej stronie, zamiast żeby scena wyglądała na martwą. log.info("perverzija: brak xtremestream na %s, probuje embed-iframe", page_url) from app.extractors.tubes import _embed_iframe try: return _embed_iframe.extract(page_url, timeout=timeout) except Exception as e: log.info("perverzija: embed-iframe fallback tez padl %s: %s", page_url, e) return None host, vid = m.group("host"), m.group("id") player_url = f"https://{host}/player/index.php?data={vid}" out: list[StreamSource] = [] for q in _QUALITIES: out.append( StreamSource( link=f"https://{host}/player/xs1.php?data={vid}&q={q}", quality=f"{q}p", type="m3u8", referer=player_url, raw={ "extractor": "perverzija", "player": player_url, "q": q, # Segmenty NIE są IP-bound i nie potrzebują Referera: segment pobrany # z domowego łącza po manifeście wziętym na VPS oddał 200 i 76 kB # zaczynające się od 0x47 (sync MPEG-TS), test 2026-08-20. Dzięki temu # przez VPS idzie sam manifest (~16 kB) przez /proxy/hls, a segmenty # telefon ciągnie prosto z xspcdn. Bez tej flagi całe wideo szło proxy. "mobile_direct_ok": True, }, ) ) return out or None