goon/app/connectors/direct_scrapers/watchporn.py
goon-foss 015bd5bb7a feat(watchporn): re-enable as KVS browse scraper + native mp4 extractor
watchporn.to was disabled 2026-05-12 (all iframes were DoodStream/CAPTCHA
-> black-screen WebView). The site has since been rebuilt as a KVS tube:
the DoodStream hosters are gone, scenes now expose flashvars get_file
direct mp4. Rewrote the dead ?s= search scraper as a /latest-updates/
browse scraper (og:title, JSON-LD duration/uploadDate, /models/ performers,
/tags/ tags, /categories/ studio) and added a KVS get_file extractor
(porntrex-style same-session 302 resolve; token not IP-bound, plays from
VPS). Moved to ALL_BROWSE_SCRAPERS, extractor swapped from _embed_iframe.
Verified: 35 fresh scenes/page (today), multi-performer, playback 1080p mp4 206.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:32:36 +02:00

108 lines
4.2 KiB
Python

"""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/<slug>/` (pomijamy numeryczne id-slugi), nazwa z tekstu linku
- tagi: `/tags/<slug>/`
- studio: pierwszy `/categories/<slug>/` (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*"([^"]+)"')
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))
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
)
],
)