"""Przypisuje WŁAŚCIWĄ osobę tam, gdzie scena dostała sam człon jej nazwiska. W bazie siedzi siedem performerów o nazwie jednoczłonowej („James", „Blue", „Moon", „Crystal", „Cruz", „Starr", „Knight"), którzy zbierają sceny wielu różnych osób dzielących ten człon: „Blue" ma sceny Anity Blue, Delilah Blue i Bonnie Blue, „Starr" ma Violet, Natashę i Bobbi Starr. **To NIE jest ten sam przypadek co `prune_token_overlap_attributions`** i tamten skrypt z założenia ich nie rusza: dla nazwy jednoczłonowej za dowód uznaje samo wystąpienie tego słowa w tytule, więc „James" jest „udokumentowany" przez każdą scenę wspominającą jakiegokolwiek Jamesa. Stąd 0 skasowanych z tych siedmiu przy sprzątaniu 2026-08-13. **I nie jest to kasowanie, tylko ODZYSKIWANIE.** W próbce 14/14 właściwa osoba NIE była przypisana do sceny, czyli Bonnie Blue nie miała tych scen na swojej stronie. Skrypt dokłada właściwego performera i zdejmuje mononim. Kryterium (wszystkie warunki naraz): 1. przypisanie mononimu pochodzi ze scrapera (kanon nietykalny; z 605 przypisań tych siedmiu 518 jest ze scrapera, reszta z TPDB/StashDB i tej nie dotykamy), 2. w bazie istnieje performer, którego nazwa KOŃCZY SIĘ tym członem po spacji („% blue" łapie „Bonnie Blue", nie łapie „Blue"), 3. ta pełna nazwa występuje w tytule sceny jako ciąg całych słów. Warunek 3 jest tym, co odróżnia odzyskanie od zgadywania: nie wystarczy, że ktoś taki istnieje, jego nazwisko musi stać w tytule tej konkretnej sceny. Gdy w tytule stoją DWIE takie osoby („Bonnie Blue and Mick Blue"), dokładamy obie i zdejmujemy mononim raz. To poprawne, bo scena faktycznie ma ich obu. Uruchomienie (kontener worker): python -m scripts.fix_mononym_attributions # dry-run python -m scripts.fix_mononym_attributions --yes # wykonaj """ from __future__ import annotations import argparse import logging from sqlalchemy import text from app.db import session_scope log = logging.getLogger(__name__) #: Nazwy jednoczłonowe zbierające cudze sceny. Lista jawna, NIE heurystyka: mononimy #: bywają prawdziwe (StashDB ma performerki „Blue" i „Cruz"), więc nie chcemy tu trafić #: przypadkiem w kogoś, kto naprawdę występuje pod jednym imieniem. MONONYMS = ("James", "Blue", "Moon", "Crystal", "Cruz", "Starr", "Knight") _BUILD = """ CREATE TEMP TABLE kand ON COMMIT DROP AS SELECT sp.scene_id, sp.performer_id AS mono_id, sp.source_id, p2.id AS wlasciwy_id FROM scene_performers sp JOIN sources src ON src.id = sp.source_id AND src.name = 'tube-scraper' JOIN performers m ON m.id = sp.performer_id AND m.canonical_name = ANY(:names) JOIN scenes s ON s.id = sp.scene_id JOIN performers p2 ON lower(p2.canonical_name) LIKE '% ' || lower(m.canonical_name) AND p2.id <> m.id -- pełna nazwa musi stać w tytule jako ciąg CAŁYCH słów; `strpos` po znormalizowanym, -- obustronnie odspacjowanym tekście, żeby „Mia Blue" nie łapało się w „Miami Blues" AND strpos(' ' || regexp_replace(lower(s.title), '[^a-z0-9]+', ' ', 'g') || ' ', ' ' || regexp_replace(lower(p2.canonical_name), '[^a-z0-9]+', ' ', 'g') || ' ') > 0 """ #: Dokładamy właściwą osobę. `ON CONFLICT DO NOTHING`, bo bywa już przypisana. #: `source_id` przepisujemy z wiersza mononimu: przypisanie pochodzi z tego samego #: scrapera, tylko trafiło w zły rekord. _ATTACH = """ INSERT INTO scene_performers (scene_id, performer_id, source_id) SELECT DISTINCT scene_id, wlasciwy_id, source_id FROM kand ON CONFLICT DO NOTHING """ _ARCHIVE = """ INSERT INTO scene_performers_pruned (scene_id, performer_id, source_id, performer_name, scene_title) SELECT DISTINCT sp.scene_id, sp.performer_id, sp.source_id, p.canonical_name, s.title FROM kand k JOIN scene_performers sp ON sp.scene_id = k.scene_id AND sp.performer_id = k.mono_id JOIN performers p ON p.id = sp.performer_id JOIN scenes s ON s.id = sp.scene_id ON CONFLICT DO NOTHING """ _DETACH = """ DELETE FROM scene_performers sp USING (SELECT DISTINCT scene_id, mono_id FROM kand) d WHERE sp.scene_id = d.scene_id AND sp.performer_id = d.mono_id """ _REPORT = """ SELECT m.canonical_name AS mononim, count(*) AS przypisan, count(DISTINCT k.wlasciwy_id) AS roznych_osob FROM kand k JOIN performers m ON m.id = k.mono_id GROUP BY m.canonical_name ORDER BY 2 DESC """ def run(*, apply: bool) -> dict[str, int]: with session_scope() as session: session.execute(text(_BUILD), {"names": list(MONONYMS)}) rows = session.execute(text(_REPORT)).all() for name, n, people in rows: print(f" {name:9} {n:>4} przypisań → {people} różnych osób", flush=True) total = sum(r[1] for r in rows) if not apply: print(f"\nRAZEM: {total} do naprawy (dry-run)") return {"matched": total, "attached": 0, "detached": 0} attached = session.execute(text(_ATTACH)).rowcount or 0 session.execute(text(_ARCHIVE)) detached = session.execute(text(_DETACH)).rowcount or 0 print(f"\nRAZEM: dołożono {attached} właściwych przypisań, zdjęto {detached} mononimów") return {"matched": total, "attached": attached, "detached": detached} def main() -> None: logging.basicConfig(level=logging.INFO, format="%(message)s") ap = argparse.ArgumentParser() ap.add_argument("--yes", action="store_true", help="wykonaj (bez tego dry-run)") run(apply=ap.parse_args().yes) if __name__ == "__main__": main()