"""One-off migration of an existing pool to the Jellyfin naming convention. Older pools carry files like ``Title.ext`` / ``Title (rok).ext`` with **no TMDb id**. This matches each such movie against TMDb by title + year and proposes a ``tmdbid`` (and the missing year, which TMDb supplies), so the file can be renamed to ``Title (rok) [tmdbid].ext`` and Jellyfin can match it deterministically. The classification is deliberately **conservative**: only an exact (diacritics-insensitive) title match with a matching year and a single candidate is ``auto``; everything else is ``ambiguous`` for the user to confirm. """ import re import unicodedata from dataclasses import dataclass, field @dataclass class MatchProposal: """One movie's proposed TMDb match.""" file: object # core.file.File current_name: str title: str year: str | None status: str # "auto" | "ambiguous" | "no_match" | "error" chosen: object = None # tmdb.TmdbResult preselected for the user, or None candidates: list = field(default_factory=list) # list[tmdb.TmdbResult] error: str = "" note: str = "" # e.g. "přes ČSFD: The Shawshank Redemption (1994)" def normalize(text: str) -> str: """Fold a title for comparison: strip diacritics/punctuation, lowercase.""" text = unicodedata.normalize("NFD", text or "") text = "".join(c for c in text if not unicodedata.combining(c)) text = re.sub(r"[^0-9a-zA-Z ]+", " ", text.lower()) return re.sub(r"\s+", " ", text).strip() def _year_close(a: str | None, b: str | None) -> bool: if not (a and b and str(a).isdigit() and str(b).isdigit()): return False return abs(int(a) - int(b)) <= 1 def classify(title: str, year: str | None, candidates: list) -> tuple[str, object]: """Return ``(status, chosen)`` for a title/year against TMDb candidates. ``auto`` requires an exact normalized-title match, a year within ±1, and that exactly one candidate qualifies. Otherwise ``ambiguous`` (top hit preselected) or ``no_match`` when there are no candidates. """ if not candidates: return "no_match", None nt = normalize(title) exact_year = [ c for c in candidates if normalize(c.title) == nt and _year_close(c.year, year) ] if len(exact_year) == 1: return "auto", exact_year[0] return "ambiguous", candidates[0]