Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping
This commit is contained in:
+46
-10
@@ -4,16 +4,32 @@ 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).
|
||||
lets two same-named films coexist and makes the filename self-describing.
|
||||
|
||||
A 3D movie additionally carries a ``.3D.<FORMAT>`` tag right before the
|
||||
extension (Jellyfin's own convention — https://jellyfin.org/docs/general/server/media/movies/):
|
||||
``Title (YYYY) [tmdbid-N].3D.HSBS.ext``. ``FORMAT`` is one of
|
||||
:data:`THREE_D_FORMATS`; Jellyfin reads it case-insensitively but Curator always
|
||||
writes it upper-case for a single canonical form.
|
||||
|
||||
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.
|
||||
# Jellyfin's recognised 3D format tags (case-insensitive on read; Curator
|
||||
# always writes upper-case). hsbs/fsbs = half/full side-by-side, htab/ftab =
|
||||
# half/full top-and-bottom, mvc = Multiview Video Coding.
|
||||
THREE_D_FORMATS = ("HSBS", "FSBS", "HTAB", "FTAB", "MVC")
|
||||
|
||||
# A trailing four-digit year in parentheses, then an optional ``[tmdbid-N]`` and
|
||||
# an optional ``.3D.<FORMAT>`` tag. 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<title>.+?) \((?P<year>\d{4})\)(?: \[tmdbid-(?P<id>\d+)\])?$")
|
||||
r"^(?P<title>.+?) \((?P<year>\d{4})\)(?: \[tmdbid-(?P<id>\d+)\])?"
|
||||
r"(?:\.3D\.(?P<format3d>hsbs|fsbs|htab|ftab|mvc))?$",
|
||||
re.IGNORECASE)
|
||||
|
||||
|
||||
def is_valid_year(year: object) -> bool:
|
||||
@@ -21,12 +37,16 @@ def is_valid_year(year: object) -> bool:
|
||||
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).
|
||||
def canonical_pool_stem(
|
||||
title: str, year: object, tmdb_id: object = None, format_3d: object = None,
|
||||
) -> str:
|
||||
"""Build the canonical pool stem ``Title (YYYY)`` (+ `` [tmdbid-N]`` /
|
||||
``.3D.<FORMAT>`` if given).
|
||||
|
||||
Raises:
|
||||
ValueError: the title is empty, the year is not four digits, or a given
|
||||
``tmdb_id`` is not a positive integer.
|
||||
ValueError: the title is empty, the year is not four digits, a given
|
||||
``tmdb_id`` is not a positive integer, or ``format_3d`` isn't one
|
||||
of :data:`THREE_D_FORMATS`.
|
||||
"""
|
||||
title = (title or "").strip()
|
||||
year_s = str(year).strip()
|
||||
@@ -40,6 +60,13 @@ def canonical_pool_stem(title: str, year: object, tmdb_id: object = None) -> str
|
||||
if not id_s.isdigit():
|
||||
raise ValueError(f"TMDb ID musí být číslo, ne „{tmdb_id}“.")
|
||||
stem += f" [tmdbid-{id_s}]"
|
||||
if format_3d is not None and str(format_3d).strip() != "":
|
||||
fmt = str(format_3d).strip().upper()
|
||||
if fmt not in THREE_D_FORMATS:
|
||||
raise ValueError(
|
||||
f"3D formát musí být jedno z {', '.join(THREE_D_FORMATS)}, "
|
||||
f"ne „{format_3d}“.")
|
||||
stem += f".3D.{fmt}"
|
||||
return stem
|
||||
|
||||
|
||||
@@ -63,6 +90,15 @@ def parse_tmdb_id(stem: str) -> int | None:
|
||||
return int(match.group("id"))
|
||||
|
||||
|
||||
def parse_3d_format(stem: str) -> str | None:
|
||||
"""Read the ``.3D.<FORMAT>`` tag from a canonical stem (upper-cased), or
|
||||
``None`` if absent."""
|
||||
match = _POOL_STEM_RE.match((stem or "").strip())
|
||||
if not match or not match.group("format3d"):
|
||||
return None
|
||||
return match.group("format3d").upper()
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user