"""watchporn.to — KVS engine direct stream extractor (re-enabled 2026-07-02). Site przebudowany na KVS: detail page ma flashvars `video_url`/`video_alt_url*`, każdy to `get_file///_p.mp4/?v-acctoken=...`. Ten sam wzorzec co porntrex. get_file 302 → finalny CDN url; resolvujemy same-session (token bywa cookie/session-bound) i oddajemy finalny url per jakość. Token nie IP-bound (cross-IP 206 z VPS zweryfikowane 2026-07-02) → mobile gra direct, zero proxy. Wcześniejszy DoodStream-CAPTCHA zniknął. """ from __future__ import annotations import logging import re import time from app.extractors._fetch import _DEFAULT_IMPERSONATE, _DEFAULT_UA, _HAS_CURL_CFFI, fetch_tube_html from app.extractors._models import HosterDead, StreamSource log = logging.getLogger(__name__) _BASE = "https://watchporn.to" _DEAD_RE = re.compile( r"this video (?:was|has been) deleted|video (?:was|has been) removed" r"|no longer available|video is unavailable", re.IGNORECASE, ) _URL_RE = re.compile( r"(video(?:_alt)?_url\d*)\s*:\s*'(https?://[^']+/get_file/[^']+)'", re.IGNORECASE, ) _TEXT_RE = re.compile( r"(video(?:_alt)?_url\d*)_text\s*:\s*'([^']*)'", re.IGNORECASE, ) 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: 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("watchporn: get_file resolve failed (%s): %s", get_file_url[:60], e) return None if status >= 400 or "/get_file/" in final: log.info("watchporn: 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: session = None if _HAS_CURL_CFFI: 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_text = resp.text if resp.status_code < 400 else "" except Exception as e: log.info("watchporn: page fetch failed %s: %s", page_url, e) html_text = "" if not html_text: html_text = fetch_tube_html(page_url, timeout=timeout) session = None else: html_text = fetch_tube_html(page_url, timeout=timeout) if html_text and _DEAD_RE.search(html_text): raise HosterDead(f"watchporn {page_url}: video deleted/removed") quality_by_var: dict[str, str] = {} for m in _TEXT_RE.finditer(html_text): quality_by_var[m.group(1).lower()] = m.group(2).strip() seen: set[str] = set() result: list[StreamSource] = [] for m in _URL_RE.finditer(html_text): var_name = m.group(1).lower() url = m.group(2) if url in seen: continue seen.add(url) quality = quality_by_var.get(var_name) final_link = url if session is not None: resolved = _resolve_get_file(session, url, timeout) if resolved: final_link = resolved result.append( StreamSource( link=final_link, type="mp4", quality=quality or None, referer=_BASE + "/", raw={"mobile_direct_ok": True}, ) ) if not result: log.info("watchporn: no KVS video_url in flashvars on %s", page_url) return None result.sort(key=lambda s: _quality_rank(s.quality), reverse=True) return result