Some checks are pending
Backend tests / test (push) Waiting to run
find_movie_candidates bral dowolne 50 wierszy z sumy trigramu i bloku studio+rok. Duze studio ma kilkaset filmow w trzech latach, wiec wierny duplikat wypadal z puli i powstawal nowy film. Teraz kolejnosc po similarity(). Skrypt remerge dostal tryb --same-title-studio (scalono 930 par o tym samym tytule i studiu). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
200 lines
8.2 KiB
Python
200 lines
8.2 KiB
Python
"""Jednorazowy dedup filmow po poprawkach oceny z 2026-10-16 (zgloszenie f8c9486e).
|
|
|
|
Ocena (movie_score) liczy teraz cyfry rzymskie jak arabskie ("Hardcore Gangbangs VIII"
|
|
= "... 8") i uznaje za zgodne studia o tej samej nazwie bez spacji ("EvilAngel" z xtapes
|
|
i "Evil Angel"). Istniejace pary, ktore ingest wtedy rozdzielil, zostaja rozdzielone,
|
|
wiec ten skrypt przechodzi tylko po parach, na ktore poprawka ma wplyw, i scala te z
|
|
ocena >= AUTO_THRESHOLD. Pary odrzucone przez czlowieka (merge_candidates.rejected)
|
|
pomija. Zostaje starszy film (jego tytul i id), mlodszy jest wlewany.
|
|
|
|
python3 /srv/scripts/remerge_movies_roman_studio.py # dry-run
|
|
python3 /srv/scripts/remerge_movies_roman_studio.py --apply
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import uuid
|
|
|
|
from sqlalchemy import select, text
|
|
|
|
from app.db import session_scope
|
|
from app.models.merge_candidate import MergeCandidate, MergeKind, MergeStatus
|
|
from app.models.movie import Movie, MoviePerformer
|
|
from app.resolve.movie_match import find_movie_candidates
|
|
from app.resolve.movie_merge import MovieMergeError, merge_movies
|
|
from app.resolve.movie_score import (
|
|
AUTO_THRESHOLD,
|
|
_roman_to_arabic,
|
|
_same_studio_name,
|
|
score_movie_candidate,
|
|
)
|
|
|
|
|
|
def _affected_ids(session) -> list[uuid.UUID]:
|
|
"""Filmy z cyfra rzymska w tytule albo ze studiem, ktore ma blizniaka bez spacji."""
|
|
rows = session.execute(text("""
|
|
WITH twins AS (
|
|
SELECT a.id FROM studios a JOIN studios b
|
|
ON a.id <> b.id
|
|
AND replace(a.name_normalized, ' ', '') = replace(b.name_normalized, ' ', '')
|
|
AND length(replace(a.name_normalized, ' ', '')) >= 4
|
|
)
|
|
SELECT m.id FROM movies m
|
|
WHERE m.title_normalized ~ '(^| )(x{0,3})(ix|iv|v?i{0,3})( |$)'
|
|
OR m.studio_id IN (SELECT id FROM twins)
|
|
ORDER BY m.created_at DESC
|
|
""")).scalars().all()
|
|
return list(rows)
|
|
|
|
|
|
def _rejected_pairs(session) -> set[frozenset]:
|
|
rows = session.execute(
|
|
select(MergeCandidate.left_id, MergeCandidate.right_id).where(
|
|
MergeCandidate.kind == MergeKind.movie,
|
|
MergeCandidate.status == MergeStatus.rejected,
|
|
)
|
|
).all()
|
|
return {frozenset((a, b)) for a, b in rows}
|
|
|
|
|
|
def _keep_rank(session, movie: Movie) -> tuple:
|
|
"""Zostaje film z rokiem, potem z liczniejsza obsada, potem starszy. Sam wiek dawal
|
|
np. "Hard Core Gangbangs 8" bez roku zamiast "Hardcore Gangbangs VIII" z TPDB."""
|
|
cast = session.execute(
|
|
text("SELECT count(*) FROM movie_performers WHERE movie_id = :m"), {"m": movie.id}
|
|
).scalar_one()
|
|
return (movie.release_year is not None, cast, -movie.created_at.timestamp())
|
|
|
|
|
|
def _same_title_studio(apply: bool) -> None:
|
|
"""Grupy filmow o identycznym tytule i studiu. find_movie_candidates bral dowolne 50
|
|
kandydatow bez kolejnosci (do 2026-10-16), wiec przy duzych studiach wierny duplikat
|
|
wypadal z puli. Kazdy czlonek grupy oceniany wzgledem zostajacego; ten sam prog co
|
|
ingest, wiec wznowienia z innym rokiem (ocena max 0,8) zostaja osobno."""
|
|
with session_scope() as s:
|
|
groups = s.execute(text("""
|
|
SELECT array_agg(id) FROM movies
|
|
WHERE studio_id IS NOT NULL AND length(title_normalized) >= 4
|
|
GROUP BY title_normalized, studio_id HAVING count(*) > 1
|
|
""")).scalars().all()
|
|
rejected = _rejected_pairs(s)
|
|
print(f"grup: {len(groups)}", flush=True)
|
|
merged = 0
|
|
for ids in groups:
|
|
with session_scope() as s:
|
|
members = sorted((s.get(Movie, i) for i in ids), key=lambda x: _keep_rank(s, x), reverse=True)
|
|
keep = members[0]
|
|
for m in members[1:]:
|
|
if frozenset((keep.id, m.id)) in rejected:
|
|
continue
|
|
perfs = [
|
|
r.performer_id
|
|
for r in s.execute(
|
|
MoviePerformer.__table__.select().where(MoviePerformer.movie_id == m.id)
|
|
).fetchall()
|
|
]
|
|
b = score_movie_candidate(
|
|
s,
|
|
candidate=keep,
|
|
norm_title_normalized=m.title_normalized,
|
|
norm_release_year=m.release_year,
|
|
norm_studio_id=m.studio_id,
|
|
norm_performer_ids=perfs,
|
|
)
|
|
if b.composite < AUTO_THRESHOLD:
|
|
continue
|
|
print(
|
|
f"{b.composite:.2f} ZOSTAJE {keep.title!r} ({keep.release_year})"
|
|
f" <= {m.title!r} ({m.release_year})",
|
|
flush=True,
|
|
)
|
|
if apply:
|
|
try:
|
|
merge_movies(s, keep_id=keep.id, drop_id=m.id, resolved_by="remerge_same_title_studio_20261016")
|
|
except MovieMergeError as e:
|
|
print(" blad:", e, flush=True)
|
|
continue
|
|
merged += 1
|
|
print(f"{'scalono' if apply else 'do scalenia'}: {merged}", flush=True)
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--apply", action="store_true")
|
|
ap.add_argument("--limit", type=int, default=0)
|
|
ap.add_argument("--same-title-studio", action="store_true")
|
|
args = ap.parse_args()
|
|
if args.same_title_studio:
|
|
_same_title_studio(args.apply)
|
|
return
|
|
|
|
with session_scope() as s:
|
|
ids = _affected_ids(s)
|
|
rejected = _rejected_pairs(s)
|
|
if args.limit:
|
|
ids = ids[: args.limit]
|
|
print(f"filmy do sprawdzenia: {len(ids)}", flush=True)
|
|
|
|
gone: set[uuid.UUID] = set()
|
|
merged = 0
|
|
for n, mid in enumerate(ids, 1):
|
|
if n % 1000 == 0:
|
|
print(f"... {n}/{len(ids)}, par: {merged}", flush=True)
|
|
if mid in gone:
|
|
continue
|
|
with session_scope() as s:
|
|
m = s.get(Movie, mid)
|
|
if m is None or not m.title_normalized:
|
|
continue
|
|
perfs = [
|
|
r.performer_id
|
|
for r in s.execute(
|
|
MoviePerformer.__table__.select().where(MoviePerformer.movie_id == m.id)
|
|
).fetchall()
|
|
]
|
|
best, best_b = None, None
|
|
for c in find_movie_candidates(
|
|
s, title_normalized=m.title_normalized, studio_id=m.studio_id, release_year=m.release_year
|
|
):
|
|
if c.id == m.id or c.id in gone or frozenset((c.id, m.id)) in rejected:
|
|
continue
|
|
roman = _roman_to_arabic(c.title_normalized or "") != (c.title_normalized or "") or (
|
|
_roman_to_arabic(m.title_normalized) != m.title_normalized
|
|
)
|
|
twin_studio = bool(
|
|
c.studio_id and m.studio_id and c.studio_id != m.studio_id
|
|
and _same_studio_name(s, c.studio_id, m.studio_id)
|
|
)
|
|
if not (roman or twin_studio):
|
|
continue # ta para wyglada dla oceny tak samo jak przy ingescie
|
|
b = score_movie_candidate(
|
|
s,
|
|
candidate=c,
|
|
norm_title_normalized=m.title_normalized,
|
|
norm_release_year=m.release_year,
|
|
norm_studio_id=m.studio_id,
|
|
norm_performer_ids=perfs,
|
|
)
|
|
if b.composite >= AUTO_THRESHOLD and (best_b is None or b.composite > best_b.composite):
|
|
best, best_b = c, b
|
|
if best is None:
|
|
continue
|
|
keep, drop = sorted((best, m), key=lambda x: _keep_rank(s, x), reverse=True)
|
|
print(
|
|
f"{best_b.composite:.2f} ZOSTAJE {keep.title!r} ({keep.release_year})"
|
|
f" <= {drop.title!r} ({drop.release_year})",
|
|
flush=True,
|
|
)
|
|
if args.apply:
|
|
try:
|
|
merge_movies(s, keep_id=keep.id, drop_id=drop.id, resolved_by="remerge_roman_studio_20261016")
|
|
except MovieMergeError as e:
|
|
print(" blad:", e, flush=True)
|
|
continue
|
|
gone.add(drop.id)
|
|
merged += 1
|
|
print(f"{'scalono' if args.apply else 'do scalenia'}: {merged}", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|