"""watchporn.to — browse scraper (KVS engine). Re-enabled 2026-07-02. Był search-scraperem (`?s=`), zamarzł, a potem site przebudowano na KVS z nowym layoutem. DoodStream-CAPTCHA (powód wyłączenia 2026-05-12) zniknął — teraz KVS flashvars `get_file` direct mp4 (extractor `watchporn`, VPS-side, token nie IP-bound). Browse `/latest-updates/` → detail page: - title: og:title ("Studio/Creator - Scene Title") - duration + release_date: JSON-LD "duration" (ISO) + "uploadDate" - performerzy: `/models//` (pomijamy numeryczne id-slugi), nazwa z tekstu linku - tagi: `/tags//` - studio: pierwszy `/categories//` (np. EvilAngel, ManyVids) """ from __future__ import annotations import html import re from app.connectors.base import ( RawPerformer, RawPlaybackSource, RawScene, RawStudio, RawTag, ) from app.connectors.direct_scrapers._browse_base import BaseBrowseScraper, meta_content from app.connectors.direct_scrapers._playtube import _parse_iso_date, _parse_iso_duration from app.normalize.text import slugify _BASE = "https://watchporn.to" _SCENE_URL_RE = re.compile(r'href="(https://watchporn\.to/video/\d+/[a-z0-9\-]+/)"', re.IGNORECASE) _MODEL_RE = re.compile(r'href="https://watchporn\.to/models/([a-z0-9\-]+)/"[^>]*>([^<]+)', re.IGNORECASE) _TAG_RE = re.compile(r'href="https://watchporn\.to/tags/([a-z0-9\-]+)/"[^>]*>([^<]+)', re.IGNORECASE) _CAT_RE = re.compile(r'href="https://watchporn\.to/categories/([a-z0-9\-]+)/"[^>]*>([^<]+)', re.IGNORECASE) _DUR_RE = re.compile(r'"duration"\s*:\s*"([^"]+)"') _UPLOAD_RE = re.compile(r'"uploadDate"\s*:\s*"([^"]+)"') _VIDEO_ID_RE = re.compile(r"/video/(\d+)/") def _derive_thumb(scene_url: str) -> str | None: """KVS trzyma poster pod stałym wzorem `contents/videos_screenshots// /preview.jpg` (zweryfikowane). Fallback gdy detail page nie ma og:image, żeby kafelek na liście miał miniaturkę OD RAZU (bez czekania na auto-enrich w SceneDetail).""" m = _VIDEO_ID_RE.search(scene_url) if not m: return None vid = int(m.group(1)) return f"{_BASE}/contents/videos_screenshots/{vid // 1000 * 1000}/{vid}/preview.jpg" class WatchPornScraper(BaseBrowseScraper): sitetag = "watchporn" def _listing_url(self, page: int) -> str: return f"{_BASE}/latest-updates/" if page <= 1 else f"{_BASE}/latest-updates/{page}/" def _extract_scene_urls(self, listing_html: str) -> list[str]: seen: set[str] = set() out: list[str] = [] for m in _SCENE_URL_RE.finditer(listing_html): url = m.group(1) if url not in seen: seen.add(url) out.append(url) return out def _parse_detail(self, scene_url: str, detail_html: str) -> RawScene | None: title = (meta_content(detail_html, property="og:title") or "").strip() if not title: return None dm = _DUR_RE.search(detail_html) duration_sec = _parse_iso_duration(dm.group(1)) if dm else None um = _UPLOAD_RE.search(detail_html) release_date = _parse_iso_date(um.group(1)) if um else None performers: list[RawPerformer] = [] seen_p: set[str] = set() for m in _MODEL_RE.finditer(detail_html): slug = m.group(1) name = html.unescape(m.group(2)).strip() if slug.isdigit() or slug in seen_p or not name: continue seen_p.add(slug) performers.append(RawPerformer(external_id=f"{self.sitetag}:model:{slug}", name=name)) tags: list[RawTag] = [] seen_t: set[str] = set() for m in _TAG_RE.finditer(detail_html): slug = m.group(1) name = html.unescape(m.group(2)).strip() if slug in seen_t or not name: continue seen_t.add(slug) tags.append(RawTag(external_id=f"{self.sitetag}:tag:{slug}", name=name, slug=slug)) thumbnail_url = ( meta_content(detail_html, property="og:image") or _derive_thumb(scene_url) ) studio: RawStudio | None = None cm = _CAT_RE.search(detail_html) if cm: cname = html.unescape(cm.group(2)).strip() if cname: studio = RawStudio( external_id=f"{self.sitetag}:studio:{slugify(cname)}", name=cname, slug=slugify(cname) ) return RawScene( external_id=f"{self.sitetag}:{scene_url}", title=title, release_date=release_date, duration_sec=duration_sec, url=scene_url, studio=studio, performers=performers, tags=tags, playback_sources=[ RawPlaybackSource( origin=f"tube:{self.sitetag}", page_url=scene_url, duration_sec=duration_sec, thumbnail_url=thumbnail_url, ) ], )