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()