"""eporner.com — direct stream extractor. Page → wyciągnij `EP.video.player.vid` + `EP.video.player.hash` → XHR `/xhr/video/?hash=` → JSON z multi-quality mp4 URL'ami (240p–1080p). Bez hasha gvideo.eporner.com URL'e zwracają 403 (są podpisane krótkoterminowymi tokenami które XHR zwraca świeże). Algorytm hex→base36 ekwiwalentny temu z aplikacji AIO Streamer (`rf1.java`): BigInteger(hex[0:8],16).toString(36) + BigInteger(hex[8:16],16).toString(36) + BigInteger(hex[16:24],16).toString(36) + BigInteger(hex[24:32],16).toString(36) URL-e są bound do IP requestera (więc resolver musi pobiegać z VPS-a, nie z klienta) i ważne ~kilka godzin. """ from __future__ import annotations import json import logging import re from app.extractors._fetch import _DEFAULT_UA, browser_get, fetch_tube_html from app.extractors._models import StreamSource log = logging.getLogger(__name__) _VID_RE = re.compile(r"EP\.video\.player\.vid\s*=\s*'([^']+)'") _HASH_RE = re.compile(r"EP\.video\.player\.hash\s*=\s*'([0-9a-fA-F]{32})'") def _hash_to_b36(hex_hash: str) -> str: """Konwertuje 32-znakowy hex hash na base36 w 4 chunkach po 8 znaków.""" parts = [] for i in (0, 8, 16, 24): n = int(hex_hash[i : i + 8], 16) if n == 0: parts.append("0") continue s = "" while n > 0: d = n % 36 ch = chr(ord("0") + d) if d < 10 else chr(ord("a") + d - 10) s = ch + s n //= 36 parts.append(s) return "".join(parts) def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: html = fetch_tube_html(page_url, timeout=timeout) m_vid = _VID_RE.search(html) m_hash = _HASH_RE.search(html) if not m_vid or not m_hash: log.warning("eporner: no vid/hash in %s", page_url) return None vid = m_vid.group(1) hash_b36 = _hash_to_b36(m_hash.group(1)) xhr_url = ( f"https://www.eporner.com/xhr/video/{vid}?hash={hash_b36}" "&domain=www.eporner.com&pixelRatio=1&playerWidth=0&playerHeight=0" "&fallback=false&embed=false&supportedFormats=mp4" ) headers = { "User-Agent": _DEFAULT_UA, "Referer": page_url, "Accept": "*/*", "X-Requested-With": "XMLHttpRequest", } try: r = browser_get(xhr_url, headers=headers, timeout=timeout, follow_redirects=True) r.raise_for_status() data = json.loads(r.text) except Exception as e: log.warning("eporner xhr fetch %s failed: %s", xhr_url, e) return None if not data.get("available", True): log.info("eporner xhr %s: available=false", vid) return None mp4_dict = (data.get("sources") or {}).get("mp4") or {} sources: list[StreamSource] = [] for label, info in mp4_dict.items(): if not isinstance(info, dict): continue src = info.get("src") if not src: continue sources.append(StreamSource(link=src, quality=label, type="mp4")) if not sources: log.warning("eporner xhr %s: no mp4 sources in JSON", vid) return None return sources