Add edit dialogs, library organizer, and TMDb pool migration

This commit is contained in:
2026-09-08 07:37:40 +02:00
parent 39712d7283
commit 79c9e34a5d
10 changed files with 1371 additions and 110 deletions
+62
View File
@@ -0,0 +1,62 @@
"""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
from typing import Optional
@dataclass
class MatchProposal:
"""One movie's proposed TMDb match."""
file: object # core.file.File
current_name: str
title: str
year: Optional[str]
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 = ""
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: Optional[str], b: Optional[str]) -> 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: Optional[str], 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]