Migrate from Poetry and pyenv to uv
This commit is contained in:
+57
-22
@@ -27,13 +27,13 @@ documented ``.3D.<FORMAT>`` spelling whether it sits flat or grouped.
|
||||
Subtitles are not media entries — they ride along with their video by filename
|
||||
stem (see :mod:`src.core.subtitles`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .naming import THREE_D_FORMATS, canonical_pool_stem, is_valid_year
|
||||
|
||||
@@ -47,10 +47,11 @@ DEFAULT_VERSION_LABEL = "Výchozí"
|
||||
|
||||
# "HSBS" / "hsbs" / "3D HSBS" / "3d.hsbs" / "3D_HSBS" → "HSBS"
|
||||
_LABEL_3D_RE = re.compile(
|
||||
rf"^(?:3d[\s._-]*)?({'|'.join(THREE_D_FORMATS)})$", re.IGNORECASE)
|
||||
rf"^(?:3d[\s._-]*)?({'|'.join(THREE_D_FORMATS)})$", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def normalize_3d_label(label: object) -> Optional[str]:
|
||||
def normalize_3d_label(label: object) -> str | None:
|
||||
"""The 3D format a version label denotes (upper-case), or ``None``.
|
||||
|
||||
Lets a plain label double as the 3D marker — ``"3D HSBS"`` typed in the
|
||||
@@ -60,6 +61,29 @@ def normalize_3d_label(label: object) -> Optional[str]:
|
||||
return match.group(1).upper() if match else None
|
||||
|
||||
|
||||
# Jellyfin's multi-part ("stacking") keywords + a number: "part1", "CD 2", "pt-3".
|
||||
_LABEL_PART_RE = re.compile(
|
||||
r"^(?:part|pt|cd|dvd|disc|disk)[\s._-]*(\d{1,2})$", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def normalize_part_label(label: object) -> int | None:
|
||||
"""The part number a version label denotes, or ``None``.
|
||||
|
||||
A *part* is one piece of a film split across several files (played back
|
||||
to back as one movie), unlike a *version*, which is an alternative. Any of
|
||||
Jellyfin's stacking spellings counts — ``part2``, ``CD2``, ``pt 2`` — and
|
||||
Curator always writes it back as ``-part<N>`` (see :func:`part_label`).
|
||||
"""
|
||||
match = _LABEL_PART_RE.match(str(label or "").strip())
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def part_label(number: int) -> str:
|
||||
"""Canonical version label of part ``number`` — ``"part<N>"``."""
|
||||
return f"part{int(number)}"
|
||||
|
||||
|
||||
def media_filename(stem: str, label: str, ext: str, *, grouped: bool) -> str:
|
||||
"""Filename for one media file of a movie whose canonical stem is ``stem``.
|
||||
|
||||
@@ -92,8 +116,9 @@ def identity_key(tmdb_id: object = None, title: str = "", year: object = None) -
|
||||
return f"tmdb-{int(id_s)}"
|
||||
raw = f"{(title or '').strip()} {str(year or '').strip()}".strip().lower()
|
||||
# Fold diacritics so "Pelíšky" and "Pelisky" can't become two movies.
|
||||
raw = "".join(c for c in unicodedata.normalize("NFD", raw)
|
||||
if not unicodedata.combining(c))
|
||||
raw = "".join(
|
||||
c for c in unicodedata.normalize("NFD", raw) if not unicodedata.combining(c)
|
||||
)
|
||||
slug = re.sub(r"[^0-9a-z]+", "-", raw).strip("-")
|
||||
return f"name-{slug or 'bez-nazvu'}"
|
||||
|
||||
@@ -107,7 +132,7 @@ class MediaFile:
|
||||
role: str = ROLE_VIDEO
|
||||
|
||||
@property
|
||||
def format_3d(self) -> Optional[str]:
|
||||
def format_3d(self) -> str | None:
|
||||
"""The 3D format this version's label denotes, if any."""
|
||||
return normalize_3d_label(self.label)
|
||||
|
||||
@@ -120,7 +145,7 @@ class MediaFile:
|
||||
return {"role": self.role, "label": self.label, "file": rel}
|
||||
|
||||
@classmethod
|
||||
def from_record(cls, data: dict, pool_dir: Path) -> "MediaFile":
|
||||
def from_record(cls, data: dict, pool_dir: Path) -> MediaFile:
|
||||
return cls(
|
||||
path=pool_dir / str(data.get("file", "")),
|
||||
label=str(data.get("label", "") or ""),
|
||||
@@ -134,19 +159,19 @@ class Movie:
|
||||
|
||||
key: str
|
||||
title: str = ""
|
||||
year: Optional[str] = None
|
||||
tmdb_id: Optional[int] = None
|
||||
year: str | None = None
|
||||
tmdb_id: int | None = None
|
||||
library: str = ""
|
||||
media: list[MediaFile] = field(default_factory=list)
|
||||
|
||||
# --- metadata that used to live on the "primary" File ---
|
||||
tags: list = field(default_factory=list)
|
||||
auto_tag_paths: set[str] = field(default_factory=set)
|
||||
csfd_url: Optional[str] = None
|
||||
date: Optional[str] = None
|
||||
added: Optional[str] = None
|
||||
csfd_url: str | None = None
|
||||
date: str | None = None
|
||||
added: str | None = None
|
||||
attributes: dict[str, str] = field(default_factory=dict)
|
||||
meta: Optional[dict] = None
|
||||
meta: dict | None = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Identity / shape
|
||||
@@ -157,7 +182,8 @@ class Movie:
|
||||
"""Playable versions, in a stable order (labelled ones by label)."""
|
||||
return sorted(
|
||||
(m for m in self.media if m.role == ROLE_VIDEO),
|
||||
key=lambda m: m.label.casefold())
|
||||
key=lambda m: m.label.casefold(),
|
||||
)
|
||||
|
||||
@property
|
||||
def is_grouped(self) -> bool:
|
||||
@@ -179,7 +205,7 @@ class Movie:
|
||||
return title
|
||||
|
||||
@property
|
||||
def primary(self) -> Optional[MediaFile]:
|
||||
def primary(self) -> MediaFile | None:
|
||||
"""The version that represents the movie (first by :attr:`videos`)."""
|
||||
videos = self.videos
|
||||
return videos[0] if videos else None
|
||||
@@ -196,8 +222,13 @@ class Movie:
|
||||
grouped = self.is_grouped
|
||||
folder = base / self.stem if grouped else base
|
||||
return [
|
||||
(m, folder / media_filename(
|
||||
self.stem, m.label, Path(m.path).suffix, grouped=grouped))
|
||||
(
|
||||
m,
|
||||
folder
|
||||
/ media_filename(
|
||||
self.stem, m.label, Path(m.path).suffix, grouped=grouped
|
||||
),
|
||||
)
|
||||
for m in self.videos
|
||||
]
|
||||
|
||||
@@ -226,16 +257,20 @@ class Movie:
|
||||
@classmethod
|
||||
def from_record(
|
||||
cls, key: str, data: dict, pool_dir: Path, tagmanager=None
|
||||
) -> "Movie":
|
||||
) -> Movie:
|
||||
movie = cls(
|
||||
key=key,
|
||||
title=str(data.get("title") or ""),
|
||||
year=(str(data["year"]) if data.get("year") else None),
|
||||
tmdb_id=(int(data["tmdb_id"]) if str(data.get("tmdb_id") or "").isdigit()
|
||||
else None),
|
||||
tmdb_id=(
|
||||
int(data["tmdb_id"])
|
||||
if str(data.get("tmdb_id") or "").isdigit()
|
||||
else None
|
||||
),
|
||||
library=str(data.get("library") or ""),
|
||||
media=[MediaFile.from_record(m, pool_dir)
|
||||
for m in (data.get("media") or [])],
|
||||
media=[
|
||||
MediaFile.from_record(m, pool_dir) for m in (data.get("media") or [])
|
||||
],
|
||||
auto_tag_paths=set(data.get("auto_tags") or []),
|
||||
csfd_url=data.get("csfd_url") or None,
|
||||
date=data.get("date") or None,
|
||||
|
||||
Reference in New Issue
Block a user