Zgloszenie 2026-08-13: strona Tiffani Time pelna scen o Tiffany Tatum, Tiffani Madison i Victorii Tiffani. Przyczyna: `_search_base.search` dokleja szukanego performera do KAZDEJ sceny bezwarunkowo (`all_performers = [RawPerformer(name=query), ...]`), wiec cala poprawnosc obsady wisi na filtrze tokenow. A filtr testowal PODCIAG na luznych tokenach, wiec: "tiffany" ⊂ "lp-s-tiffany-love-first-time-gonzo" (inna osoba) "time" ⊂ "adult-time", "first-time", "multiple-times" (pospolite slowo) Historia tej linijki to trzy poziomy: any() -> all() -> (teraz) fraza. `all()` naprawil "Ava Koxxx na scenach Avy Devine", ale zostawil oba powyzsze. Teraz slug musi zawierac CALA nazwe jako ciag, z granica slowa po bokach, z dowolnym separatorem miedzy czlonami. Dozwolone koncowe "s": slug gubi apostrof, wiec "TiffaniTime's" zapisuje jako "tiffanitimes". Nie luzuje bramki, bo przed tym "s" stoi cala fraza. hqporner mial WLASNA kopie tego filtru (i wlasny zestaw dziur) - podpiety pod ta sama funkcje, zeby sie nie rozjezdzalo. 12 przypadkow testowych z realnych danych, w tym te ktore MUSZA przejsc (tiffani-time-*, tiffanitimes-*, mia-li-* z czlonem <3 znaki). Do tego skrypt czyszczacy juz istniejace pomylki. Kryterium zawezone po obejrzeniu probki: samo "brak nazwy w tytule" kasowalo MESKICH performerow (tuby nie wypisuja ich w tytule, a przypisanie szlo z listy obsady, nie z searcha). Dolozony warunek "inny performer sceny ma dowod I dzieli czlon" - probka 14/14 to realne pomylki.
130 lines
5.3 KiB
Python
130 lines
5.3 KiB
Python
"""Kasuje przypisania performerów powstałe przez CZĘŚCIOWE dopasowanie nazwy.
|
||
|
||
Mechanizm, który to produkował (naprawiony 2026-08-13 w `_search_base._slug_matches_query`):
|
||
search po nazwie doklejał szukanego performera do KAŻDEJ sceny, która przeszła filtr
|
||
tokenów, a filtr testował PODCIĄG na luźnych tokenach. Więc „Tiffany Time" łapało się
|
||
w „s-tiffany-love-first-time", a „Eliza E" w „eliza-ibarra". Zgłoszenie 2026-08-13:
|
||
strona Tiffani Time pełna scen o Tiffany Tatum, Tiffani Madison i Victorii Tiffani.
|
||
|
||
**Kryterium (trzy warunki naraz, każdy konieczny):**
|
||
|
||
1. przypisanie pochodzi ze scrapera (kanon nietykalny, `scene_performers.source_id`),
|
||
2. nazwy performera NIE ma w tytule ani URL-u sceny (brak dowodu),
|
||
3. na tej samej scenie jest INNY performer, który nazwę ma **i dzieli z nim człon**.
|
||
|
||
Trzeci warunek jest tym, co odróżnia pomyłkę od braku danych, i został dołożony po
|
||
obejrzeniu próbki. Bez niego reguła kasowała **męskich performerów**: tuby zwyczajowo
|
||
nie wypisują ich w tytule („Charles Dera" przy scenie „HouseHumpers – Demi Hawks and
|
||
Serena Hill"), a przypisanie przyszło z listy obsady, nie z searcha, więc było poprawne.
|
||
Po zawężeniu próbka 14/14 to realne pomyłki: Nikki Nevada przy Nikki Brooks, Eliza E
|
||
przy Elizie Ibarze, Miss Marisa przy Miss Fufu, CJ Sloan przy Sloan Rider.
|
||
|
||
Wsadowo po zakresach `scene_id`, bo pojedynczy DELETE po całej tabeli trzymałby locki
|
||
na `scene_performers` i zablokował ingest (zdarzyło się przy migracji 0026).
|
||
|
||
Uruchomienie (kontener worker):
|
||
python -m scripts.prune_token_overlap_attributions # dry-run
|
||
python -m scripts.prune_token_overlap_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__)
|
||
|
||
#: 16 przedziałów po pierwszym nibblu UUID-a. Każdy to osobna transakcja.
|
||
_PREFIXES = "0123456789abcdef"
|
||
|
||
_BUILD_SQL = """
|
||
CREATE TEMP TABLE ev ON COMMIT DROP AS
|
||
SELECT sp.scene_id,
|
||
sp.performer_id,
|
||
(replace(replace(replace(
|
||
lower(coalesce(ps.page_url,'') || ' ' || s.title), '-',''), '_',''), ' ','')
|
||
LIKE '%' || replace(lower(p.canonical_name),' ','') || '%') AS ma_dowod,
|
||
ARRAY(
|
||
SELECT t FROM unnest(string_to_array(lower(p.canonical_name),' ')) t
|
||
WHERE length(t) >= 3
|
||
) AS tokeny
|
||
FROM scene_performers sp
|
||
JOIN sources src ON src.id = sp.source_id AND src.name = 'tube-scraper'
|
||
JOIN performers p ON p.id = sp.performer_id
|
||
JOIN scenes s ON s.id = sp.scene_id
|
||
LEFT JOIN LATERAL (
|
||
SELECT page_url FROM playback_sources x WHERE x.scene_id = s.id LIMIT 1
|
||
) ps ON true
|
||
WHERE sp.scene_id >= CAST(:lo AS uuid) AND sp.scene_id < CAST(:hi AS uuid);
|
||
CREATE INDEX ON ev (scene_id);
|
||
ANALYZE ev;
|
||
"""
|
||
|
||
#: Kandydaci: bez dowodu, ale inny performer TEJ SCENY dowód ma i dzieli człon.
|
||
_SELECT_SQL = """
|
||
SELECT e.scene_id, e.performer_id
|
||
FROM ev e
|
||
WHERE NOT e.ma_dowod
|
||
AND EXISTS (
|
||
SELECT 1 FROM ev e2
|
||
WHERE e2.scene_id = e.scene_id
|
||
AND e2.performer_id <> e.performer_id
|
||
AND e2.ma_dowod
|
||
AND e2.tokeny && e.tokeny
|
||
)
|
||
"""
|
||
|
||
|
||
def _range(prefix: str) -> tuple[str, str]:
|
||
lo = prefix + "0000000-0000-0000-0000-000000000000"
|
||
nxt = _PREFIXES[_PREFIXES.index(prefix) + 1] if prefix != "f" else None
|
||
hi = (nxt + "0000000-0000-0000-0000-000000000000") if nxt else "ffffffff-ffff-ffff-ffff-ffffffffffff"
|
||
return lo, hi
|
||
|
||
|
||
def run(*, apply: bool) -> dict[str, int]:
|
||
total = 0
|
||
for prefix in _PREFIXES:
|
||
lo, hi = _range(prefix)
|
||
with session_scope() as session:
|
||
for stmt in _BUILD_SQL.strip().split(";\n"):
|
||
if stmt.strip():
|
||
session.execute(text(stmt), {"lo": lo, "hi": hi})
|
||
rows = session.execute(text(_SELECT_SQL)).all()
|
||
n = len(rows)
|
||
if apply and rows:
|
||
session.execute(
|
||
text(
|
||
"""
|
||
DELETE FROM scene_performers sp
|
||
USING (SELECT * FROM ev e
|
||
WHERE NOT e.ma_dowod
|
||
AND EXISTS (SELECT 1 FROM ev e2
|
||
WHERE e2.scene_id = e.scene_id
|
||
AND e2.performer_id <> e.performer_id
|
||
AND e2.ma_dowod
|
||
AND e2.tokeny && e.tokeny)) d
|
||
WHERE sp.scene_id = d.scene_id AND sp.performer_id = d.performer_id
|
||
"""
|
||
)
|
||
)
|
||
total += n
|
||
log.info("zakres %s: %d przypisan%s", prefix, n, " (skasowane)" if apply else " (dry-run)")
|
||
print(f" zakres {prefix}: {n}{' skasowanych' if apply else ' do skasowania'}", flush=True)
|
||
return {"matched": total}
|
||
|
||
|
||
def main() -> None:
|
||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--yes", action="store_true", help="wykonaj DELETE (bez tego dry-run)")
|
||
args = ap.parse_args()
|
||
out = run(apply=args.yes)
|
||
print(f"\nRAZEM: {out['matched']} {'skasowanych' if args.yes else 'do skasowania (dry-run)'}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|