"""yesporn.vip — KVS (kt_player) engine direct stream extractor. REWRITE 2026-05-31 (user "yespornvip otwiera reklamy / 410 / fallback do strony"): VPS znów dociera do yesporn.vip (HTTP 200 — odblokowane jak porntrex 2026-05-22), więc resolvujemy SERVER-SIDE zamiast WebView fallback (który pokazywał stronę + preroll-reklamę i scrape łapał reklamę zamiast wideo). KVS flashvars: `video_url` / `video_alt_url` / `video_alt_url2` (480/720/1080p), każdy w formie `function/0/https://yesporn.vip/get_file///...mp4/` + `license_code`. kt_player dekoduje HASH algorytmem license_code (permutacja pierwszych 32 znaków — identyczny algo jak yt-dlp KVS; zweryfikowany 2026-05-31 że odtwarza output kt_player). Zdekodowany `get_file` 302 → `cdn5.yesporn.vip/remote_control.php?time=&cv=` który serwuje wideo bezpośrednio (206 video/mp4). Ten finalny URL jest **time-bound signed, NIE IP/cookie-bound** — zweryfikowane cross-IP (VPS resolve → fetch z innego IP = 206). Resolvujemy 302 w tej samej sesji co fetch strony (get_file token bywa session-bound), oddajemy finalny portable CDN url per jakość. Mobile gra direct natywnie, zero WebView, zero reklam, zero VPS proxy bandwidth. """ from __future__ import annotations import logging import re import time import urllib.parse as _up from app.extractors._fetch import _DEFAULT_IMPERSONATE, _DEFAULT_UA, _HAS_CURL_CFFI, fetch_tube_html from app.extractors._models import StreamSource log = logging.getLogger(__name__) _BASE = "https://yesporn.vip" # flashvars: `video_url: 'function/0/https://.../get_file/...'` (+ alt/alt2 dla wyższych q). _URL_RE = re.compile( r"(video(?:_alt)?_url\d*)\s*:\s*'(function/0/[^']+)'", re.IGNORECASE, ) _TEXT_RE = re.compile( r"(video(?:_alt)?_url\d*)_text\s*:\s*'([^']*)'", re.IGNORECASE, ) _LICENSE_RE = re.compile(r"license_code\s*:\s*'(\$[^']+)'", re.IGNORECASE) _HASH_LENGTH = 32 def _kvs_license_token(license_code: str) -> list[int]: """kt_player license token (algo zgodny z yt-dlp KVS).""" license_code = license_code.replace("$", "") license_values = [int(c) for c in license_code] modlicense = license_code.replace("0", "1") center = len(modlicense) // 2 modlicense = str(4 * abs(int(modlicense[:center + 1]) - int(modlicense[center:])))[:center + 1] return [ (license_values[index + offset] + current) % 10 for index, current in enumerate(int(c) for c in modlicense) for offset in range(4) ] def _kvs_real_url(video_url: str, license_code: str) -> str: """Dekoduje `function/0/...get_file/N//...` permutując pierwsze 32 znaki HASH.""" if not video_url.startswith("function/0/"): return video_url parsed = _up.urlparse(video_url[len("function/0/"):]) lt = _kvs_license_token(license_code) parts = parsed.path.split("/") h = parts[3][:_HASH_LENGTH] idx = list(range(_HASH_LENGTH)) acc = 0 for src in reversed(range(_HASH_LENGTH)): acc += lt[src] dest = (src + acc) % _HASH_LENGTH idx[src], idx[dest] = idx[dest], idx[src] parts[3] = "".join(h[i] for i in idx) + parts[3][_HASH_LENGTH:] return _up.urlunparse(parsed._replace(path="/".join(parts))) def _quality_rank(label: str | None) -> int: if not label: return -1 m = re.search(r"(\d{3,4})\s*p", label, re.IGNORECASE) return int(m.group(1)) if m else -1 def _resolve_get_file(session, get_file_url: str, timeout: float) -> str | None: """Follow get_file 302 → finalny portable CDN url (w sesji z cookies strony).""" sep = "&" if "?" in get_file_url else "?" url = f"{get_file_url}{sep}rnd={int(time.time() * 1000)}" try: r = session.get( url, timeout=timeout, allow_redirects=True, stream=True, headers={"Referer": _BASE + "/", "Range": "bytes=0-1"}, ) final = str(r.url) status = r.status_code r.close() except Exception as e: log.info("yespornvip: get_file resolve failed (%s): %s", get_file_url[:60], e) return None if status >= 400 or "/get_file/" in final: log.info("yespornvip: get_file resolve bad status=%s final=%s", status, final[:70]) return None return final def extract(page_url: str, *, timeout: float = 60.0) -> list[StreamSource] | None: # Wspólna sesja: get_file token jest session-bound, więc 302 MUSI lecieć w tej samej # sesji curl_cffi co fetch strony. if not _HAS_CURL_CFFI: # Bez curl_cffi nie zrobimy same-session resolve — brak fallbacku (WebView fallback # pokazywał reklamy; lepiej None niż reklama). log.info("yespornvip: curl_cffi unavailable — cannot resolve") return None from curl_cffi import requests as _cf_requests session = _cf_requests.Session(impersonate=_DEFAULT_IMPERSONATE) try: resp = session.get( page_url, headers={"User-Agent": _DEFAULT_UA, "Accept": "text/html,application/xhtml+xml"}, timeout=timeout, allow_redirects=True, ) html = resp.text if resp.status_code < 400 else "" except Exception as e: log.info("yespornvip: page fetch failed %s: %s", page_url, e) return None if not html: log.info("yespornvip: empty page %s", page_url) return None licm = _LICENSE_RE.search(html) if not licm: log.info("yespornvip: no license_code on %s", page_url) return None license_code = licm.group(1) quality_by_var: dict[str, str] = {} for m in _TEXT_RE.finditer(html): quality_by_var[m.group(1).lower()] = m.group(2).strip() seen_dec: set[str] = set() result: list[StreamSource] = [] for m in _URL_RE.finditer(html): var_name = m.group(1).lower() decoded = _kvs_real_url(m.group(2), license_code) if decoded in seen_dec: continue seen_dec.add(decoded) final = _resolve_get_file(session, decoded, timeout) if not final: continue result.append( StreamSource( link=final, type="mp4", quality=quality_by_var.get(var_name) or None, referer=_BASE + "/", # cdn5 remote_control.php url: time-bound signed, NIE IP/cookie-bound # (zweryfikowane cross-IP) → mobile gra direct, zero proxy. raw={"mobile_direct_ok": True}, ) ) if not result: log.info("yespornvip: no resolvable get_file on %s", page_url) return None result.sort(key=lambda s: _quality_rank(s.quality), reverse=True) return result