"""sexu.com — browse scraper. Dodany 2026-07-27. Najlepsze metadane z całej tegorocznej serii kandydatów. Próbka 25 najnowszych scen: obsada 20/25 (80%), **kanał 25/25 i to realne studia** (New Sensations, Brutal X, Moms Bang Teens, 2 Girls 1 Camera), dokładna długość w sekundach 25/25, data 25/25. Świeżość: cała próbka miała 7-8 godzin. Listing daje 120 scen na stronę. Dlatego **nie ma tu bramki na obsadę** (w odróżnieniu od fullvideosporn czy 8kporner). Te 20% scen bez performera i tak niesie studio + dokładną długość + oryginalny tytuł, czyli komplet sygnałów, których resolver potrzebuje do canonical matcha. Bramka odrzuciłaby dobre dane. **Dwie pułapki w HTML, obie sprawdzone:** 1. **Tagi trzeba wyciąć z bloku.** Linków `/tag/` jest na stronie 25, ale realnych tagów sceny tylko 5 (reszta to sidebar „popularne wyszukiwania"). Bez zawężenia do bloku „Tags:" wpisalibyśmy każdej scenie te same 20 śmieci. 2. **W bloku tagów siedzą też linki `/search?q=`** — to fraz-y pod SEO („pleases", „hole", „indian milf"), nie tagi. Bierzemy wyłącznie `/tag/`. **Studio.** Kanał to zwykle studio, ale przy uploadach amatorskich bywa nazwany po performerze (`/channel/priya+rai` przy scenie z Priya Rai). Taki kanał odrzucamy, bo performer w tabeli studios to zanieczyszczenie, nie dana. Data jest względna („7 hours ago", „yesterday"), więc liczymy ją wstecz od teraz. To i tak data uploadu na tube, nie premiery studia. Playback: `POST /api/video-info` (extractor `sexu`) — wymaga Bright Data, bo Cloudflare blokuje POST z VPS. Sam scrape listingu i detali idzie bez proxy. """ from __future__ import annotations import html as html_mod import logging import re from datetime import UTC, datetime, timedelta from app.connectors.base import ( RawPerformer, RawPlaybackSource, RawScene, RawStudio, RawTag, ) from app.connectors.direct_scrapers._browse_base import BaseBrowseScraper from app.normalize.text import slugify log = logging.getLogger(__name__) _BASE = "https://sexu.com" _SCENE_HREF_RE = re.compile(r'href="(/\d+/)"') _TITLE_RE = re.compile(r"]*>\s*([^<]+?)\s*") _DURATION_RE = re.compile(r'property="video:duration"\s+content="(\d+)"') _OG_IMAGE_RE = re.compile(r'property="og:image"\s+content="([^"]+)"') _DATE_RE = re.compile(r'player-block__date">\s*([^<]+?)\s*<') _REL_DATE_RE = re.compile( r"(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago", re.IGNORECASE ) _UNIT_DAYS = { "second": 0, "minute": 0, "hour": 0, "day": 1, "week": 7, "month": 30, "year": 365, } # Bloki metadanych: `
Tags:
`. # Linki sceny są między tytułem a `_container` (tam zaczyna się formularz „Suggest…"). _BLOCK_END_RE = re.compile(r"_container|player-tags__title") def _block(detail_html: str, label: str) -> str: """Fragment HTML należący do bloku `