diff --git a/scripts/_reap_probe.py b/scripts/_reap_probe.py new file mode 100644 index 0000000..7329dee --- /dev/null +++ b/scripts/_reap_probe.py @@ -0,0 +1,127 @@ +"""Wspolna sonda dla pomiaru i przemialu martwych zrodel. + +Trzymamy to w jednym miejscu, zeby bramka w `tube_reap_dead.py` sprawdzala DOKLADNIE ta +sama sonde, ktora potem mieli baze. Gdyby to byly dwie kopie, bramka moglaby przepuscic +przemial oparty na innym zachowaniu niz to, ktore przetestowala. +""" +from __future__ import annotations + +import re +import time +from urllib.parse import urlsplit, urlunsplit + +from app.extractors import try_extract +from app.extractors._models import HosterDead, TubePageError + +ALIVE, DEAD, UNKNOWN = "alive", "dead", "unknown" + +_SPACING_SEC = 0.4 + +# Ciag cyfr dosc dlugi, zeby byl numerem sceny, a nie fragmentem tytulu. +_NUM_RE = re.compile(r"\d{4,}") +# Segment wygladajacy na identyfikator, a nie na tytul: bez mysinikow i dosc dlugi, +# za to z cyframi (np. eporner `000goU7Teov`, xnxx `video-10003hb8`). +_ID_SEG_RE = re.compile(r"^[A-Za-z0-9_.-]{6,}$") +_ZNACZNIK = "99899989" + + +def bogus_url(url: str) -> str | None: + """Prawdziwy adres -> adres sceny, ktora na pewno nie istnieje. + + Sluzy do kontroli negatywnej: jesli taki adres wraca ZYWY, to znaczy, ze dla tego + tuba w ogole nie widzimy smierci i jego wynik trzeba odrzucic, a nie czytac jako + "brak trupow". + + Pierwsza wersja podmieniala po prostu ostatni ciag cyfr, przez co nie budowala nic + dla tubow BEZ numeru sceny w adresie (`hentaigasm.com/1-funkan-dake-.../`, + `mypornerleak.com/1st-double-vag-.../`). Takie tuby wychodzily w raporcie jako "0/0", + czyli niezmierzone, a bylo ich okolo 950 tys. zrodel. Dlatego kolejno: + + 1. numer w parametrach zapytania (`siska.video/video.php?videoID=100007`), + 2. segment sciezki bedacy identyfikatorem (`/video/39776/`, `/386900.html`, + `/hd-porn/000goU7Teov/`, `/video-10003hb8/`), + 3. gdy identyfikatora nie ma, tub routuje po samym slugu, wiec psujemy slug. + + Kolejnosc ma znaczenie: slug psujemy DOPIERO na koncu, bo czesc tubow routuje po + samym numerze i sluga nie sprawdza, wiec zepsuty slug oddalby 200 i falszywie + wygladal na slepote sondy. + """ + parts = urlsplit(url) + + if parts.query and _NUM_RE.search(parts.query): + q = _NUM_RE.sub(_ZNACZNIK, parts.query, count=1) + return urlunsplit((parts.scheme, parts.netloc, parts.path, q, parts.fragment)) + + segs = parts.path.split("/") + idx = [i for i, s in enumerate(segs) if s] + if not idx: + return None + + def _rozbij(seg: str) -> tuple[str, str]: + rdzen, _, ext = seg.rpartition(".") + return (rdzen, ext) if rdzen else (seg, "") + + def _zloz(i: int, nowy: str, ext: str) -> str: + segs[i] = f"{nowy}.{ext}" if ext else nowy + return urlunsplit( + (parts.scheme, parts.netloc, "/".join(segs), parts.query, parts.fragment) + ) + + # Przebieg 1: segment bedacy CALY identyfikatorem (`/video/39776/`, `/386900.html`, + # `/hd-porn/000goU7Teov/`). Segmenty czysto literowe pomijamy, bo to nazwa trasy + # (`videos`, `episodes`, `watch`). Podmiana trasy sprawdzalaby, czy tub obsluguje + # dany adres, a nie czy istnieje scena, wiec tub oddajacy 404 na zla trase i 200 na + # nieistniejaca scene FALSZYWIE zdalby bramke. To jedyny kierunek bledu, ktory boli. + for i in reversed(idx): + rdzen, ext = _rozbij(segs[i]) + if "-" in rdzen or not _ID_SEG_RE.match(rdzen): + continue + if rdzen.isdigit() or (rdzen.isalnum() and any(c.isdigit() for c in rdzen)): + nowy = ( + _NUM_RE.sub(_ZNACZNIK, rdzen, count=1) + if _NUM_RE.search(rdzen) + else rdzen[:-2] + "zq" + ) + return _zloz(i, nowy, ext) + + # Przebieg 2: identyfikator wtopiony w slug (`/video-10003hb8/`, + # `/videos/01082023-a-bj-to-remember/`, `..._17800085.html`). + for i in reversed(idx): + rdzen, ext = _rozbij(segs[i]) + if _NUM_RE.search(rdzen): + return _zloz(i, _NUM_RE.sub(_ZNACZNIK, rdzen, count=1), ext) + + # Przebieg 3: identyfikatora nie ma, tub routuje po samym slugu, wiec psujemy slug. + last = idx[-1] + rdzen, _, ext = segs[last].rpartition(".") + if rdzen: + segs[last] = f"{rdzen}-zqx9nieistnieje.{ext}" + else: + segs[last] = f"{segs[last]}-zqx9nieistnieje" + return urlunsplit((parts.scheme, parts.netloc, "/".join(segs), parts.query, parts.fragment)) + + +def probe(sitetag: str, url: str) -> tuple[str, str]: + """(werdykt, szczegol) dla jednego adresu. Szczegol sluzy tylko do raportu. + + Idzie przez `try_extract`, czyli ten sam kod, ktorego uzywa aplikacja przy + odtwarzaniu, wiec sonda nie moze "widziec" czegos innego niz user. + + DEAD pada TYLKO na twardych sygnalach: HTTP 404/410 albo `HosterDead` (soft-404, np. + sxyprn "Post Not Found"). Wszystko inne, lacznie z pustym wynikiem ekstraktora, to + UNKNOWN i zrodla nikt nie rusza. Odwrotne ustawienie schowaloby zywe sceny po cichej + zmianie markupu, tak jak przy porntrex (6479 scen). + """ + time.sleep(_SPACING_SEC) + try: + out = try_extract(sitetag, url) + except TubePageError as e: + if e.status_code in (404, 410): + return DEAD, f"HTTP {e.status_code}" + # 403, 5xx, rate limit: NIE jest dowodem na nic. + return UNKNOWN, f"HTTP {e.status_code}" + except HosterDead: + return DEAD, "soft-404" + except Exception as e: + return UNKNOWN, type(e).__name__ + return (ALIVE, "") if out else (UNKNOWN, "ekstraktor zwrocil pusto")