"""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}