diff --git a/app/config.py b/app/config.py index 9fae463..009a721 100644 --- a/app/config.py +++ b/app/config.py @@ -123,6 +123,12 @@ class Settings(BaseSettings): sched_phash_blacklist_hours: int = Field( default=24, validation_alias="GOON_SCHED_PHASH_BLACKLIST_HOURS" ) + # Śmieciowi performerzy — kategorie i studia podszywające się pod osoby („Creampie", + # „Natural tits", „Brazzers"). Audyt 2026-07-27: 277 rekordów, 33 784 przypisania. + # Odrastają, bo tube-search dokleja performera po jednym tokenie. 24h. 0 = off. + sched_junk_performers_hours: int = Field( + default=24, validation_alias="GOON_SCHED_JUNK_PERFORMERS_HOURS" + ) # Ingest freshness watchdog — alert do Sentry gdy aktywny tube (origin # tube:) przestał dawać nowe sceny > próg. Łapie zamrożenie # pojedynczego origin, którego globalny monitor (jeden Source "tube-scraper") nie diff --git a/app/scheduler/jobs.py b/app/scheduler/jobs.py index 95198c6..e858c2f 100644 --- a/app/scheduler/jobs.py +++ b/app/scheduler/jobs.py @@ -292,6 +292,21 @@ def _job_phash_blacklist() -> None: log.exception("[scheduler] phash blacklist failed") +def _job_junk_performers() -> None: + """Kasuje kategorie i studia podszywające się pod performerów („Creampie", + „Natural tits", „Brazzers"). Audyt 2026-07-27: 277 rekordów, 33 784 przypisania. + Odrastają, bo tube-search dokleja performera po jednym tokenie z zapytania, a + resolve_performer tworzy go z dowolnej nazwy podanej przez tube.""" + log.info("[scheduler] junk performers starting") + try: + from app.scheduler.junk_performers import run_junk_performer_cleanup + + _run_with_timeout(run_junk_performer_cleanup, label="junk-performers") + log.info("[scheduler] junk performers done") + except Exception: + log.exception("[scheduler] junk performers failed") + + def _job_title_duration_dedup() -> None: """Scal missing-merge dupy (ten sam performer + identyczny tytuł + długość) których bulk_dedup nie łapie (tube re-scrape / cross-tube np. porn00 vs xnxx, reports @@ -498,6 +513,17 @@ def build_scheduler(cfg: dict[str, Any]) -> BlockingScheduler: ) log.info("scheduler: phash blacklist every %dh", cfg["phash_blacklist_hours"]) + if cfg.get("junk_performers_hours"): + sched.add_job( + _job_junk_performers, + IntervalTrigger(hours=cfg["junk_performers_hours"], start_date=INTERVAL_ANCHOR), + id="junk_performers", + replace_existing=True, + max_instances=1, + coalesce=True, + ) + log.info("scheduler: junk performers every %dh", cfg["junk_performers_hours"]) + if cfg.get("title_dedup_hours"): sched.add_job( _job_title_duration_dedup, diff --git a/app/scheduler/junk_performers.py b/app/scheduler/junk_performers.py new file mode 100644 index 0000000..9d1a281 --- /dev/null +++ b/app/scheduler/junk_performers.py @@ -0,0 +1,91 @@ +"""Kasowanie śmieciowych performerów — kategorii i studiów podszywających się pod osoby. + +Skąd się biorą: `_search_base` filtruje wyniki tube-searcha warunkiem „slug zawiera +≥1 token z zapytania", a `resolve_performer` tworzy performera z dowolnej nazwy podanej +przez tube. W efekcie „Creampie", „Natural tits", „reverse cowgirl", „69", „Brazzers" +i „Blacked" wylądowały w tabeli `performers`. Audyt 2026-07-27: 277 takich rekordów z +33 784 przypisaniami do scen. + +**Kryterium jest wąskie celowo, bo szerokie kasuje ludzi.** Kryterium używane w +`app/api/scenes.py` do de-rankingu (slug pokrywa się z tagiem I brak refa tpdb/stashdb) +daje 3111 trafień, ale przy próbkowaniu okazało się mieć ~60% fałszywek dla nazw +wielowyrazowych: wpadają w nie **Hatano Yui, Davina Raines, Michelle Ferrari, Clanddi, +SolaZola, Kate Kuray**. Powód: założenie „prawdziwy performer ma ref kanoniczny" nie +działa dla JAV, amatorek i debiutantek, których nie ma w TPDB. Do de-rankingu to +wystarcza (fałszywka = gorszy ranking), do kasowania nie. + +Rozstrzyga dopiero **promiskuityczność taga**: ile RÓŻNYCH innych performerów pojawia +się na scenach otagowanych tym samym slugiem. Nazwisko współwystępuje głównie ze swoim +właścicielem i partnerami ze sceny, kategoria z całym katalogiem. Zmierzone: + + prawdziwe osoby Hatano Yui 0, Clanddi 0, Chyna 0, Kwini 2, Katekuray 9, + SolaZola 4, Julia Reaves 20 (maksimum 20) + kategorie Deepthroat 39541, Creampie 33661, riding 16284, Skinny 9443 + +Próg 50 leży ponad dwukrotnie powyżej najwyższej prawdziwej osoby. Dodatkowo chronimy +nazwy w formacie „Imię Nazwisko", bo sygnał studia (odrzucony) wciągał marki osobiste. + +Job, nie bramka w `resolve_performer`: kasowanie cyklem nie dotyka gorącej ścieżki +ingestu, a śmieci i tak odrastają wolno. Ta sama logika co przy [[phash_blacklist]]. +""" +from __future__ import annotations + +import logging + +from sqlalchemy import text +from sqlalchemy.orm import Session + +log = logging.getLogger(__name__) + +#: Ponad 2× powyżej najwyższej zmierzonej prawdziwej osoby (Julia Reaves, 20). +DEFAULT_MIN_FOREIGN_PERFORMERS = 50 + +_SELECT_SQL = """ +WITH junk AS ( + SELECT pf.id, pf.canonical_name, pf.slug + FROM performers pf + WHERE EXISTS (SELECT 1 FROM tags t WHERE t.slug = pf.slug) + AND NOT EXISTS ( + SELECT 1 FROM performer_external_refs r JOIN sources s ON s.id = r.source_id + WHERE r.performer_id = pf.id AND s.kind IN ('tpdb', 'stashdb') + ) + -- „Imię Nazwisko" zostaje: marki osobiste (Kate Kuray, Julia Reaves) wyglądają + -- jak studio, ale są osobami. + AND pf.canonical_name !~ '^[A-Z][a-z]+ [A-Z][a-z]+$' +) +SELECT j.id FROM junk j +WHERE ( + SELECT count(DISTINCT sp.performer_id) + FROM tags t JOIN scene_tags st ON st.tag_id = t.id + JOIN scene_performers sp ON sp.scene_id = st.scene_id + WHERE t.slug = j.slug AND sp.performer_id <> j.id +) >= :min_foreign +""" + + +def find_junk_performers(session: Session, *, min_foreign: int = DEFAULT_MIN_FOREIGN_PERFORMERS): + return [r[0] for r in session.execute(text(_SELECT_SQL), {"min_foreign": min_foreign})] + + +def run_junk_performer_cleanup( + *, min_foreign: int = DEFAULT_MIN_FOREIGN_PERFORMERS, commit: bool = True +) -> dict[str, int]: + """Skasuj śmieciowych performerów. `scene_performers` leci kaskadą — sceny i tagi + zostają, więc treść nie ginie: „creampie" nadal jest tagiem przy 176 tys. scen, + tylko przestaje udawać osobę.""" + from app.db import session_scope + + with session_scope() as session: + ids = find_junk_performers(session, min_foreign=min_foreign) + if not ids: + return {"found": 0, "deleted": 0} + deleted = 0 + if commit: + deleted = int( + session.execute( + text("DELETE FROM performers WHERE id = ANY(:ids)"), {"ids": ids} + ).rowcount or 0 + ) + session.commit() + log.info("junk performers: znaleziono %d, skasowano %d", len(ids), deleted) + return {"found": len(ids), "deleted": deleted} diff --git a/app/scheduler/worker.py b/app/scheduler/worker.py index 041c9de..5db04b4 100644 --- a/app/scheduler/worker.py +++ b/app/scheduler/worker.py @@ -219,6 +219,9 @@ def run_forever() -> int: # Blacklista zdegenerowanych phashy — zaślepki dzielone przez setki scen (audyt # 2026-07-27). Bez tego zaślepka jest zawsze najbliższym trafieniem w Path 3. "phash_blacklist_hours": getattr(settings, "sched_phash_blacklist_hours", 24) or None, + # Śmieciowi performerzy — kategorie/studia udające osoby (audyt 2026-07-27). + # Kryterium wąskie celowo: szerokie kasuje realnych ludzi bez refa w TPDB. + "junk_performers_hours": getattr(settings, "sched_junk_performers_hours", 24) or None, # Taxonomy scene_count refresh — denormalizacja liczników (perf fix 0019). "taxonomy_counts_hours": getattr(settings, "sched_taxonomy_counts_hours", 3) or None, # Ingest freshness watchdog — per-origin alert do Sentry (report 14f3a655).