diff --git a/app/resolve/movie_score.py b/app/resolve/movie_score.py index 9465f67..8b7897d 100644 --- a/app/resolve/movie_score.py +++ b/app/resolve/movie_score.py @@ -58,6 +58,55 @@ AUTO_THRESHOLD = 0.85 REVIEW_THRESHOLD = 0.65 +def _series_number(title_normalized: str) -> int | None: + """Ostatnia samodzielna liczba w tytule, z pominieciem lat (1900-2099). + + "her way to use tinder 61" -> 61, "the doctor is in 2" -> 2, "the doctor" -> None, + "cece 2019" -> None (rok to nie numer odcinka). + """ + liczby = [ + int(t) + for t in title_normalized.split() + if t.isdigit() and not 1900 <= int(t) <= 2099 + ] + return liczby[-1] if liczby else None + + +# Tyle najwyzej dostaje tytul, gdy numer serii ma tylko jedna strona. Ponizej progu +# auto-merge, zeby sam tytul nie decydowal, ale dosc, zeby ze zgodnym studiem i rokiem +# trafic do recznego przegladu zamiast od razu odpadac. +_ONE_SIDED_NUMBER_CAP = 0.6 + + +def _title_signal(cand_title: str, inc_title: str) -> tuple[float, bool, dict]: + """(ocena tytulu 0..1, twarde odrzucenie, powody). + + `token_set_ratio` mierzy czesc wspolna, wiec tytul bedacy podzbiorem drugiego dostaje + pelne 100: "the doctor" vs "the doctor is in 2", "fantasies" vs "dark fantasies 3". + Numerowane serie wychodza na 90-96%, tez ponad progiem auto-merge. Gdy zrodlo nie ma + roku ani studia (lustra dooplay czesto nie maja), o wszystkim decyduje sam tytul i + cala seria zapada sie w jeden wpis. Zgloszenie 32100b94 (2026-08-30): pod "The Doctor" + siedzialo 9 roznych filmow z pandamovies, w tym granny porn; w bazie 1878 filmow ma + co najmniej dwie rozne strony z pandamovies, a jedna to norma. + + Zasady, obie oparte na numerze serii (ostatnia liczba w tytule, bez lat): + * oba tytuly maja numer i sie rozni -> twarde odrzucenie (to inny odcinek), + * numer ma tylko jedna strona -> tytul najwyzej 0.6 (inne wydanie, nie lustro). + Sam podzbior bez numerow zostaje bez kary celowo: "cece" vs "watch cece adult" to + prawdziwe lustro z przedrostkiem SEO i to wlasnie dla niego jest token_set. + """ + ocena = fuzz.token_set_ratio(cand_title, inc_title) / 100.0 + powody: dict = {} + n_c, n_i = _series_number(cand_title), _series_number(inc_title) + if n_c is not None and n_i is not None and n_c != n_i: + powody["series_number_mismatch_reject"] = [n_c, n_i] + return ocena, True, powody + if (n_c is None) != (n_i is None): + powody["series_number_one_sided_cap"] = n_c if n_c is not None else n_i + return min(ocena, _ONE_SIDED_NUMBER_CAP), False, powody + return ocena, False, powody + + def score_movie_candidate( session: Session, *, @@ -71,10 +120,12 @@ def score_movie_candidate( breakdown = MovieScoreBreakdown(reasons={}) # Title (always) + series_reject = False if candidate.title_normalized and norm_title_normalized: - breakdown.title = fuzz.token_set_ratio( + breakdown.title, series_reject, powody = _title_signal( candidate.title_normalized, norm_title_normalized - ) / 100.0 + ) + breakdown.reasons.update(powody) # Year, exact lub ±1 = wysoka, ±2 = średnia if candidate.release_year is not None and norm_release_year is not None: @@ -141,6 +192,11 @@ def score_movie_candidate( breakdown.reasons["studio_year_mismatch_reject"] = True score = 0.0 + # Hard reject: rozne numery serii. Bez tego "Her way to use Tinder 61" wchlaniala + # "... Tinder 60" (token_set 96%), az pod jednym tytulem siedzialy 102 strony. + if series_reject: + score = 0.0 + breakdown.composite = max(0.0, min(1.0, score)) breakdown.reasons["weights"] = norm_w breakdown.reasons["sub_scores"] = available diff --git a/scripts/browse_catchup.py b/scripts/browse_catchup.py new file mode 100644 index 0000000..b3655ca --- /dev/null +++ b/scripts/browse_catchup.py @@ -0,0 +1,60 @@ +"""Jednorazowe nadrobienie zaleglosci jednego browse-scrapera (wiecej stron niz w cyklu). + +Po co: gdy tub byl przez jakis czas nieosiagalny (porndoe: 10 dni HTTP 403 na IP +serwerowni, 2026-08-21..31), regularny cykl browse-latest bierze tylko 5 stron i nie +dosiegnie tego, co sie nazbieralo. Ten skrypt robi jeden przebieg na N stron dla +JEDNEGO sitetagu, tym samym kodem co scheduler (`_ingest_iter_into_run`), wiec wynik +laduje w tych samych tabelach i liczy sie tak samo. + +PULAPKA, przez ktora ten plik powstal: `docker compose run -d ... python3 -u -` ze +skryptem podanym przez heredoc na stdin NIE DZIALA. Przy `-d` stdin nie jest domkniety, +wiec python czeka na wejscie w nieskonczonosc: kontener "Up 2 days", zero logu, zero +zapisow, a z zewnatrz wyglada jak dlugie zadanie. Odpalaj skrypty z pliku, po sciezce. + +Uzycie (osobny kontener, restart workera go nie ubija): + docker compose run -d --rm --name goon-catchup-porndoe worker \ + python3 -u /srv/scripts/browse_catchup.py --sitetag porndoecom --pages 14 +""" +from __future__ import annotations + +import argparse +import logging +import sys + +from app.connectors.base import SourceKind +from app.connectors.direct_scrapers import ALL_BROWSE_SCRAPERS +from app.scheduler.browse_latest import SCRAPER_SOURCE_NAME, _ingest_iter_into_run + +log = logging.getLogger("browse-catchup") + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser() + p.add_argument("--sitetag", required=True, help="np. porndoecom") + p.add_argument("--pages", type=int, default=10) + a = p.parse_args(argv) + + logging.basicConfig( + level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", stream=sys.stdout + ) + for noisy in ("httpx", "httpcore", "urllib3", "curl_cffi"): + logging.getLogger(noisy).setLevel(logging.WARNING) + + klasa = next((c for c in ALL_BROWSE_SCRAPERS if c.sitetag == a.sitetag), None) + if klasa is None: + log.error("nie ma browse-scrapera o sitetag=%s", a.sitetag) + return 2 + scraper = klasa() + log.info("catchup %s: %d stron", a.sitetag, a.pages) + c = _ingest_iter_into_run( + source_kind=SourceKind.scraper, + source_name=SCRAPER_SOURCE_NAME, + run_label=f"browse-catchup:{a.sitetag}", + iterator_factory=lambda: scraper.latest_scenes(max_pages=a.pages), + ) + log.info("KONIEC catchup %s: %s", a.sitetag, c) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())