"""Jednorazowe nadrobienie katalogu superporn.com z sitemapy. Dlaczego w ogóle: deep-crawl przemielił listing HTML do końca (kursor 548, status `exhausted`), a mimo to sitemapa ma sceny, których nie mamy. Pomiar 2026-08-20 na ośmiu submapach po 60 losowych adresów: **46% angielskich URL-i nieznanych**, przy czym w nowszych submapach nawet 75%. Przy około 29 tys. angielskich adresów daje to rzędu 13 tys. scen do odzyskania. Listing po prostu nie wystawia całego katalogu. Dlaczego skrypt, a nie job w schedulerze: superporn NIC już nie publikuje. Submapy są ułożone chronologicznie, a najnowszy wpis ma datę 2025-01-28, czyli serwis stoi od ponad roku. To jest nadrobienie zaległości raz, nie stała maszyneria. Wznawianie za darmo: bierzemy wyłącznie adresy, których nie ma w `playback_sources`, więc ponowne odpalenie samo pomija to, co już weszło. Żadnego pliku stanu. Sceny lecą z `backfill=True`, bo to katalog z lat 2022-2025. Bez tego stary import udawałby świeżość w „+N nowych" i w ulubionych (patrz `scenes.backfill`). Użycie (w kontenerze workera): python3 /srv/scripts/superporn_sitemap_backfill.py --limit 200 --dry-run python3 /srv/scripts/superporn_sitemap_backfill.py """ from __future__ import annotations import argparse import logging import re import sys import time from concurrent.futures import ThreadPoolExecutor from sqlalchemy import text from app.connectors.direct_scrapers import ALL_BROWSE_SCRAPERS, SCRAPER_SOURCE_NAME from app.db import session_scope from app.extractors import browser_get from app.ingest import _process_scene, get_or_create_source from app.models.source import SourceKind log = logging.getLogger("superporn-backfill") _SITEMAP_INDEX = "https://www.superporn.com/sitemap.xml" _LOC_RE = re.compile(r"([^<]+)") # Bierzemy TYLKO angielskie adresy. Sitemapa zawiera te same sceny pod /nl/, /fr/, /es/ # i tak dalej, a to ta sama treść, nie nowa podaż. _EN_SCENE_RE = re.compile(r"^https://www\.superporn\.com/video/") # Ile stron szczegółów ciągniemy naraz. Bright Data to ryczałt, więc kosztu za GB nie ma, # ale nie ma też powodu walić w serwis serią. _CONCURRENCY = 5 def _scraper(): by_tag = {c.sitetag: c for c in ALL_BROWSE_SCRAPERS} return by_tag["superporncom"]() def _video_submaps(sc) -> list[str]: """Submapy wideo, NAJNOWSZE PIERWSZE (numeracja rośnie chronologicznie).""" idx = browser_get(_SITEMAP_INDEX, timeout=60, proxy=sc._proxy).text maps = [m for m in _LOC_RE.findall(idx) if "videos." in m] def _num(u: str) -> int: m = re.search(r"videos\.(\d+)\.xml", u) return int(m.group(1)) if m else -1 return sorted(maps, key=_num, reverse=True) def _unknown_urls(urls: list[str]) -> list[str]: if not urls: return [] with session_scope() as s: known = { r[0] for r in s.execute( text("SELECT page_url FROM playback_sources WHERE page_url = ANY(:u)"), {"u": urls}, ).fetchall() } return [u for u in urls if u not in known] def _fetch_parse(args: tuple): sc, url = args try: r = browser_get(url, timeout=sc._timeout, proxy=sc._proxy) if r.status_code >= 400: return None return sc._parse_detail(url, r.text) except Exception as e: log.debug("detail padl %s: %s", url, e) return None def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser() p.add_argument("--limit", type=int, default=0, help="max scen do zapisania (0 = bez limitu)") p.add_argument("--dry-run", action="store_true", help="nie zapisuj, tylko policz") p.add_argument("--max-maps", type=int, default=0, help="ogranicz liczbę submap (test)") a = p.parse_args(argv) logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", stream=sys.stdout ) # Bez tego httpx loguje KAŻDY fetch miniaturki i postęp tonie w szumie. for noisy in ("httpx", "httpcore", "urllib3"): logging.getLogger(noisy).setLevel(logging.WARNING) sc = _scraper() if not sc._proxy: log.error("brak proxy, superporn bez niego oddaje CF 403; przerywam") return 1 submaps = _video_submaps(sc) if a.max_maps: submaps = submaps[: a.max_maps] log.info("submap wideo: %d (od najnowszej)", len(submaps)) with session_scope() as s: source_id = get_or_create_source( s, kind=SourceKind.scraper, name=SCRAPER_SOURCE_NAME ).id counters = {"seen": 0, "new": 0, "updated": 0, "skipped": 0, "errors": 0} saved = scanned = unknown_total = 0 t0 = time.time() for i, sm in enumerate(submaps, 1): try: body = browser_get(sm, timeout=60, proxy=sc._proxy).text except Exception as e: log.warning("submapa %s nie pobrana: %s", sm, e) continue urls = [u for u in _LOC_RE.findall(body) if _EN_SCENE_RE.match(u)] scanned += len(urls) todo = _unknown_urls(urls) unknown_total += len(todo) log.info( "[%d/%d] %s: EN=%d nieznanych=%d (zapisane dotad: %d, %.0f min)", i, len(submaps), sm.rsplit("/", 1)[-1], len(urls), len(todo), saved, (time.time() - t0) / 60, ) if not todo or a.dry_run: continue if a.limit: todo = todo[: max(0, a.limit - saved)] if not todo: break with ThreadPoolExecutor(max_workers=_CONCURRENCY) as pool: raws = list(pool.map(_fetch_parse, [(sc, u) for u in todo])) for raw in raws: if raw is None: counters["errors"] += 1 continue counters["seen"] += 1 try: # backfill=True: to katalog 2022-2025, nie ma udawać nowosci. _process_scene( source_id=source_id, raw_scene=raw, counters=counters, backfill=True ) saved += 1 except Exception: counters["errors"] += 1 log.exception("ingest padl") if a.limit and saved >= a.limit: log.info("limit %d osiagniety", a.limit) break log.info( "KONIEC: przejrzane EN=%d, nieznanych=%d, zapisane=%d, counters=%s, czas=%.0f min", scanned, unknown_total, saved, counters, (time.time() - t0) / 60, ) return 0 if __name__ == "__main__": raise SystemExit(main())