63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""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]
|