"""Pool file naming convention. Every movie in the pool is named ``Title (YYYY) [tmdbid-N].ext`` — a title, a four-digit year in parentheses, and (when known) the TMDb id in brackets, which is exactly the form Jellyfin matches on. The ``[tmdbid-N]`` part is optional: a plain ``Title (YYYY)`` is still valid (Jellyfin then matches by name). The year lets two same-named films coexist and makes the filename self-describing. These helpers build, validate and parse the stem (the filename without its extension). """ import re # A trailing four-digit year in parentheses, then an optional ``[tmdbid-N]``. The # title is non-greedy so a title that itself contains parentheses keeps them: # "Kurz (special) (2019)" → title "Kurz (special)", year 2019. _POOL_STEM_RE = re.compile( r"^(?P.+?) \((?P<year>\d{4})\)(?: \[tmdbid-(?P<id>\d+)\])?$") def is_valid_year(year: object) -> bool: """True when ``year`` is (or stringifies to) exactly four digits.""" return bool(re.fullmatch(r"\d{4}", str(year).strip())) def canonical_pool_stem(title: str, year: object, tmdb_id: object = None) -> str: """Build the canonical pool stem ``Title (YYYY)`` (+ `` [tmdbid-N]`` if given). Raises: ValueError: the title is empty, the year is not four digits, or a given ``tmdb_id`` is not a positive integer. """ title = (title or "").strip() year_s = str(year).strip() if not title: raise ValueError("Název filmu nesmí být prázdný.") if not re.fullmatch(r"\d{4}", year_s): raise ValueError(f"Rok musí být čtyřmístné číslo, ne „{year}“.") stem = f"{title} ({year_s})" if tmdb_id is not None and str(tmdb_id).strip() != "": id_s = str(tmdb_id).strip() if not id_s.isdigit(): raise ValueError(f"TMDb ID musí být číslo, ne „{tmdb_id}“.") stem += f" [tmdbid-{id_s}]" return stem def parse_pool_stem(stem: str) -> tuple[str, int] | None: """Split a canonical stem into ``(title, year)``; ``None`` if it isn't one. A trailing ``[tmdbid-N]`` is tolerated and dropped (use :func:`parse_tmdb_id` to read it), so the returned title is always the clean title. """ match = _POOL_STEM_RE.match((stem or "").strip()) if not match: return None return match.group("title").strip(), int(match.group("year")) def parse_tmdb_id(stem: str) -> int | None: """Read the ``[tmdbid-N]`` from a canonical stem, or ``None`` if absent.""" match = _POOL_STEM_RE.match((stem or "").strip()) if not match or not match.group("id"): return None return int(match.group("id")) def is_canonical_pool_stem(stem: str) -> bool: """True when ``stem`` matches the ``Title (YYYY)`` convention.""" return _POOL_STEM_RE.match((stem or "").strip()) is not None def year_from_filename(name: str) -> str | None: """Best-effort four-digit year guessed from a raw (release) filename. Prefers a parenthesised year (``… (1999) …``) and otherwise falls back to the last standalone 19xx/20xx run. Used only to pre-fill the import dialog — the user confirms it. Returns the year as a string, or ``None``. """ paren = re.search(r"\((19\d{2}|20\d{2})\)", name) if paren: return paren.group(1) loose = re.findall(r"(?<!\d)(19\d{2}|20\d{2})(?!\d)", name) return loose[-1] if loose else None