"""xtremestream (player perverzija): manifest HLS z segmentami AES-128 skladany po stronie backendu. Od ok. 2026-10-12 player (`ftk_engine.js`, zaciemniony, z blokada devtools) nie gra juz z `xs1.php`: segmenty pod starymi adresami `/cdn/down///.html` oddaja 404, a prawdziwe leza pod `/cdn/down//_enc/.html` i sa zaszyfrowane AES-128-CBC, IV = numer segmentu (16 bajtow big-endian), dopelnienie PKCS7, czyli dokladnie tak jak standardowe `#EXT-X-KEY:METHOD=AES-128` bez atrybutu IV (sprawdzone 2026-10-14: openssl z kluczem i IV=0 daje poprawny MPEG-TS, ffprobe widzi h264 i aac). Klucz (16 B) oddaje `proto_api.php?data=&op=key&vt=` z naglowkiem `X-XS-P: 1`. `vt` to token ze strony playera (`XSFTK.boot(id, vt, player)`), zwiazany z IP i UA pobierajacego (ten sam vt z innego IP = 403 `forbidden-vt`), ale klucz jest jeden na film, niezalezny od vt (sprawdzone z dwoch IP). Segmenty i stary manifest `xs1.php` (nadal serwowany, z dobrymi dlugosciami i rotacja hostow cdn[i % n]) nie sa zwiazane z IP. Dlatego telefon nic nie musi robic: backend bierze `xs1.php`, przepisuje sciezki na `_enc`, dokleja `#EXT-X-KEY` z URI wzglednym `key.bin`, a `/proxy/hls//key.bin` oddaje klucz pobrany na wlasny vt. Telefon ciagnie segmenty prosto z CDN (przez VPS leci ~50 kB manifestu i 16 B klucza). Czesc katalogu (starszy storage) ma nadal zwykle segmenty: wtedy manifest idzie bez klucza, tak jak dotad. """ from __future__ import annotations import logging import re import time from urllib.parse import parse_qs, quote, urlparse import httpx log = logging.getLogger(__name__) _UA = ( "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/128.0.0.0 Mobile Safari/537.36" ) _PLAYER_RE = re.compile( r"https?://(?P[a-z0-9\-]+\.xtremestream\.[a-z]+)/player/index\.php\?data=(?P[0-9a-f]{16,64})", re.IGNORECASE, ) _BOOT_RE = re.compile(r'XSFTK\.boot\("[0-9a-f]+",\s*"([^"]+)"') # `https://pervl7.xspcdn01.click/cdn/down//720/7200.html` -> grupy: prefiks, jakosc, numer _SEG_RE = re.compile(r"^(https?://[^/]+/cdn/down/[0-9a-f]+/)(\d+p?)/\2(\d+)\.html\s*$") _DEFAULT_QUALITY = "720" _KEY_TTL_SEC = 6 * 3600 _key_cache: dict[str, tuple[bytes, float]] = {} def parse_player(player_url: str) -> tuple[str, str] | None: m = _PLAYER_RE.search(player_url) return (m.group("host"), m.group("id")) if m else None def quality_of(player_url: str) -> str: q = parse_qs(urlparse(player_url).query).get("q", [_DEFAULT_QUALITY])[0] return q if re.fullmatch(r"\d{3,4}", q) else _DEFAULT_QUALITY def _to_enc(line: str) -> str: m = _SEG_RE.match(line) if not m: return line return f"{m.group(1)}{m.group(2)}_enc/{m.group(2)}{m.group(3)}.html" def _reachable(url: str, timeout: float) -> bool: try: r = httpx.get(url, headers={"User-Agent": _UA, "Range": "bytes=0-0"}, timeout=timeout) return r.status_code in (200, 206) except httpx.HTTPError: return False def fetch_key(player_url: str, *, timeout: float = 20.0) -> bytes | None: """16-bajtowy klucz AES filmu, z pamiecia podreczna na 6 h (klucz nie zalezy od vt).""" parsed = parse_player(player_url) if not parsed: return None host, vid = parsed cached = _key_cache.get(vid) if cached and time.time() - cached[1] < _KEY_TTL_SEC: return cached[0] index_url = f"https://{host}/player/index.php?data={vid}" try: page = httpx.get( index_url, headers={"User-Agent": _UA, "Referer": "https://tube.perverzija.com/"}, timeout=timeout, follow_redirects=True, ) m = _BOOT_RE.search(page.text) if not m: log.info("xtremestream: brak tokenu vt na %s (http %s)", index_url, page.status_code) return None vt = m.group(1) # UA musi byc ten sam co przy stronie playera, vt jest pod niego liczony. k = httpx.get( f"https://{host}/player/proto_api.php?data={vid}&op=key&vt={quote(vt, safe='')}", headers={"User-Agent": _UA, "X-XS-P": "1", "Referer": index_url}, timeout=timeout, ) except httpx.HTTPError as e: log.info("xtremestream: pobranie klucza padlo dla %s: %s", vid, e) return None if k.status_code != 200 or len(k.content) != 16: log.info("xtremestream: klucz dla %s http %s, %d B", vid, k.status_code, len(k.content)) return None _key_cache[vid] = (k.content, time.time()) return k.content def build_manifest(player_url: str, *, key_uri: str = "key.bin", timeout: float = 20.0) -> str | None: """Manifest HLS do odegrania przez telefon: segmenty `_enc` z CDN i klucz pod `key_uri` (wzgledny wobec adresu manifestu). None, gdy xs1.php nie oddaje manifestu albo zaden wariant segmentow (zaszyfrowany ani zwykly) nie odpowiada.""" parsed = parse_player(player_url) if not parsed: return None host, vid = parsed q = quality_of(player_url) index_url = f"https://{host}/player/index.php?data={vid}" try: m = httpx.get( f"https://{host}/player/xs1.php?data={vid}&q={q}", headers={"User-Agent": _UA, "Referer": index_url}, timeout=timeout, ) except httpx.HTTPError as e: log.info("xtremestream: xs1.php padlo dla %s: %s", vid, e) return None text = m.text if m.status_code != 200 or not text.lstrip(" \t\r\n").startswith("#EXTM3U"): log.info("xtremestream: xs1.php dla %s http %s, nie-manifest (%d B)", vid, m.status_code, len(m.content)) return None lines = text.splitlines() segments = [l for l in lines if l.startswith("http")] if not segments: return None first_enc = _to_enc(segments[0]) if first_enc == segments[0] or not _reachable(first_enc, timeout): # Starszy storage: zwykle segmenty, manifest bez zmian. Jesli i te nie odpowiadaja, # film jest martwy po stronie hostera. return text if _reachable(segments[0], timeout) else None out: list[str] = [] key_done = False for line in lines: out.append(_to_enc(line) if line.startswith("http") else line) if not key_done and line.startswith("#EXT-X-MEDIA-SEQUENCE"): out.append(f'#EXT-X-KEY:METHOD=AES-128,URI="{key_uri}"') key_done = True if not key_done: out.insert(1, f'#EXT-X-KEY:METHOD=AES-128,URI="{key_uri}"') return "\n".join(out) + "\n"