"""supjav.com — JAV browse scraper (przez Bright Data proxy, CF-blokuje VPS IP). Osobna pula JAV (sekcja JAV), origin `tube:supjav` w JAV_ORIGINS. supjav agreguje embedy (serwery TV/FST/ST/VOE), gdzie realny hoster jest schowany za `data-link` (reverse-hex → lk1.supremejav.com); rozwiązuje go ekstraktor `app/extractors/tubes/supjav.py`. Browse (RE 2026-07-10): - listing: homepage `/` (najnowsze ~66, WordPress). Głębsza paginacja `/page/N/` leci CF 403 nawet przez proxy → browse ogranicza się do strony 1 (scheduler odświeża). - karty: `
`. - Metadane bierzemy Z KARTY (tytuł/thumb/URL/kod JAV/data z roku-miesiąca ścieżki thumb), NIE z detalu — detal przez proxy jest flaky (CF przepuszcza ~40% IP), a per-post fetch byłby wolny i lossy. Studio/tagi (poza kodem) dorabia enrich później. supjav blokuje VPS IP twardym CF → listing idzie przez Bright Data ISP proxy z retry (`fetch_supjav_html`, rotacja IP aż trafi przepuszczający). Bez proxy scraper no-op. """ from __future__ import annotations import html as _html import logging import re from datetime import date from app.config import get_settings from app.connectors.base import RawPlaybackSource, RawScene, RawTag from app.connectors.direct_scrapers._browse_base import BaseBrowseScraper from app.extractors.tubes.supjav import fetch_supjav_html from app.normalize.text import slugify log = logging.getLogger(__name__) _BASE = "https://supjav.com" # Karta: link-obraz z href + title + thumb. title="([^"]*)" jest bezpieczne (wartość # atrybutu nie zawiera surowego "), więc tytuły z `>` (np. `<<3-Day Offer>>`) przechodzą. # Link-obraz karty: . # supjav miesza kolejność atrybutów (href/rel/class="img"/title w dwóch wariantach), więc # NIE wymagamy pozycji class="img" — wymóg `>\s*` ma tekst, nie ). Thumb eager `src=` lub lazy # `data-original=` (karty poniżej folda: src="data:..." placeholder + realny data-original). _CARD_RE = re.compile( r'href="(https://supjav\.com/\d+\.html)"[^>]*?\btitle="([^"]*)"[^>]*?>\s*' r']*?\b(?:src|data-original|data-src|data-lazy-src)="(https://img\.supjav\.com/[^"]+)"', re.IGNORECASE, ) _THUMB_DATE_RE = re.compile(r"/images/(\d{4})/(\d{2})/") # Kod JAV: FC2PPV ma cyfrę w prefiksie (FC2PPV 4931572), studia mają myślnik (MGOLD-053, # DLDSS-510, SNOS-258). Prefiks liter+cyfr, separator - lub spacja, numer. _CODE_RE = re.compile(r"^([A-Z][A-Z0-9]{1,7}[-\s]\d{2,7})", re.IGNORECASE) class SupjavScraper(BaseBrowseScraper): sitetag = "supjav" _timeout = 70.0 def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) # CF-blok VPS IP → listing przez Bright Data proxy (jak superporn). self._proxy = get_settings().brightdata_proxy_url if not self._proxy: log.warning("supjav: BRIGHTDATA_PROXY_URL unset — scraper disabled") def _listing_url(self, page: int) -> str: return f"{_BASE}/" if page <= 1 else f"{_BASE}/page/{page}/" def crawl_page(self, page: int) -> list[RawScene] | None: if not self._proxy: return [] # bez proxy gwarantowany CF 403 (exhausted) html = fetch_supjav_html(self._listing_url(page), proxy=self._proxy, timeout=self._timeout) if not html or len(html) < 15000: # sam challenge / fetch fail. page 1 = transient (None, retry potem); # page > 1 = najpewniej CF-403 głębszej paginacji = koniec (exhausted). return None if page <= 1 else [] seen: set[str] = set() out: list[RawScene] = [] for m in _CARD_RE.finditer(html): url, title_raw, thumb_raw = m.group(1), m.group(2), m.group(3) if url in seen: continue seen.add(url) raw = self._card_to_scene(url, title_raw, thumb_raw) if raw is not None: out.append(raw) return out def _card_to_scene(self, url: str, title_raw: str, thumb_raw: str) -> RawScene | None: title = _html.unescape(title_raw).strip() if not title: return None thumb = thumb_raw.split("!")[0] or None # utnij `!320x216.jpg` → pełna rozdz. release_date: date | None = None dm = _THUMB_DATE_RE.search(thumb_raw) if dm: try: release_date = date(int(dm.group(1)), int(dm.group(2)), 1) except ValueError: release_date = None tags: list[RawTag] = [] cm = _CODE_RE.match(title) if cm: code = re.sub(r"[ ]", "-", cm.group(1).upper()) tags.append(RawTag(external_id=f"javcode:{code}", name=code, slug=slugify(code))) return RawScene( external_id=f"{self.sitetag}:{url}", title=title, release_date=release_date, url=url, tags=tags, playback_sources=[ RawPlaybackSource( origin=f"tube:{self.sitetag}", page_url=url, thumbnail_url=thumb, ) ], ) # HTML detail nieużywany (browse z kart), ale ABC wymaga. def _extract_scene_urls(self, listing_html: str) -> list[str]: # pragma: no cover return [] def _parse_detail(self, scene_url: str, detail_html: str) -> RawScene | None: # pragma: no cover return None