From 5217d0722255abfecb65fbb44ebb279504aad179 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Thu, 13 Aug 2026 11:17:43 +0200 Subject: [PATCH] fix(search): nazwa performera musi byc w slugu SPOJNA FRAZA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../direct_scrapers/_search_base.py | 47 +++++-- app/connectors/direct_scrapers/hqporner.py | 9 +- scripts/prune_token_overlap_attributions.py | 130 ++++++++++++++++++ 3 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 scripts/prune_token_overlap_attributions.py diff --git a/app/connectors/direct_scrapers/_search_base.py b/app/connectors/direct_scrapers/_search_base.py index 8e62b5f..b60dc63 100644 --- a/app/connectors/direct_scrapers/_search_base.py +++ b/app/connectors/direct_scrapers/_search_base.py @@ -120,6 +120,28 @@ class BaseSearchScraper(BaseDirectTubeScraper): """ return None + @staticmethod + def _slug_matches_query(slug_lower: str, query: str) -> bool: + """Czy slug zawiera nazwę z query jako spójną frazę, z granicami słowa. + + Człony rozdzielamy dowolnym niealfanumerycznym separatorem (tube używa `-`, + `_` albo skleja), a po bokach wymagamy granicy, żeby „tiffany" nie łapało się + w „stiffany". Człony krótsze niż `_query_token_min_len` pomijamy przy budowie + frazy (np. „Mia Li"), inaczej wypadłyby wszystkie sceny tej osoby. + """ + toks = [ + re.escape(tok) + for tok in query.lower().split() + if len(tok) >= BaseSearchScraper._query_token_min_len + ] + if not toks: + return True + phrase = r"[^a-z0-9]*".join(toks) + # Dozwolone końcowe „s": slug gubi apostrof, więc dzierżawcze „TiffaniTime's" + # zapisuje jako „tiffanitimes". Nie luzuje to bramki, bo przed tym „s" i tak + # musi stać cała fraza (samo „first-times" nadal nie przejdzie dla „Tiffani Time"). + return re.search(rf"(?= 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()