"""Wykrywanie i czyszczenie zdegenerowanych phashy. Phash identyfikuje scenę tylko wtedy, gdy miniaturka pokazuje treść sceny. Gdy tube serwuje zaślepkę, czarną klatkę albo wspólne intro studia, ta sama wartość ląduje przy setkach niepowiązanych scen i przestaje cokolwiek znaczyć — a `find_by_phash_within` bierze najbliższą wartość z CAŁEJ tabeli, więc taki wpis jest czystym szumem. Próg `min_scenes=10` wynika z rozkładu zmierzonego 2026-07-27: 11 000 wartości występowało przy dokładnie 2 scenach (to realne duplikaty, chcemy je zachować), 1346 przy 3-9, a dopiero od 10 w górę zaczynają się grupy, w których liczba różnych tytułów równa się liczbie scen — czyli nic ich nie łączy poza obrazkiem. Kolejność ma znaczenie: najpierw `refresh` (zapisz wartości na stałe), potem `purge` (skasuj wiersze). Odwrotnie skasowalibyśmy dowody i blacklista wyszłaby pusta. """ from __future__ import annotations import logging from sqlalchemy import text from sqlalchemy.orm import Session log = logging.getLogger(__name__) DEFAULT_MIN_SCENES = 10 _REFRESH_SQL = """ INSERT INTO phash_blacklist (value, scene_count) SELECT value, count(DISTINCT scene_id) FROM scene_fingerprints WHERE kind = 'phash' GROUP BY value HAVING count(DISTINCT scene_id) >= :min_scenes ON CONFLICT (value) DO UPDATE SET scene_count = EXCLUDED.scene_count """ _PURGE_SQL = """ DELETE FROM scene_fingerprints f USING phash_blacklist b WHERE f.kind = 'phash' AND f.value = b.value """ def refresh_phash_blacklist(session: Session, *, min_scenes: int = DEFAULT_MIN_SCENES) -> int: """Dopisz do blacklisty wartości dzielone przez ≥min_scenes scen. Zwraca rozmiar blacklisty po odświeżeniu (wpisy raz dodane NIE znikają — o to chodzi).""" session.execute(text(_REFRESH_SQL), {"min_scenes": min_scenes}) return int(session.execute(text("SELECT count(*) FROM phash_blacklist")).scalar_one()) def purge_blacklisted_fingerprints(session: Session) -> int: """Skasuj wiersze `scene_fingerprints` o zablacklistowanej wartości. Zwraca liczbę skasowanych. Sceny zostają nietknięte — znika tylko bezużyteczny odcisk.""" return int(session.execute(text(_PURGE_SQL)).rowcount or 0) def run_phash_blacklist(*, min_scenes: int = DEFAULT_MIN_SCENES) -> dict[str, int]: from app.db import session_scope with session_scope() as session: size = refresh_phash_blacklist(session, min_scenes=min_scenes) purged = purge_blacklisted_fingerprints(session) session.commit() log.info("phash blacklist: %d wartości na liście, skasowano %d odcisków", size, purged) return {"blacklist_size": size, "purged": purged}