1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt, ' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety; zakresy 0–5 i A–Z na dywiz. Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach, czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu, tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana. Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie). Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika to zero zmiany. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
4.2 KiB
Python
125 lines
4.2 KiB
Python
"""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/<srv>/<token>/<path>_<q>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
|