Add movie-keyed pool index with multi-version movies, ČSFD title bridge, subtitles and miniDLNA library scoping

This commit is contained in:
2026-09-13 17:16:48 +02:00
parent c0ad1c1553
commit c399a6aed3
26 changed files with 4635 additions and 221 deletions
+155
View File
@@ -0,0 +1,155 @@
"""Tests for the movie object and the pool layout it derives (src.core.movie)."""
from pathlib import Path
import pytest
from src.core.movie import (
DEFAULT_VERSION_LABEL,
MediaFile,
Movie,
identity_key,
media_filename,
normalize_3d_label,
)
@pytest.mark.parametrize("label,expected", [
("HSBS", "HSBS"), ("hsbs", "HSBS"), ("3D HSBS", "HSBS"),
("3d.hsbs", "HSBS"), ("3D_FTAB", "FTAB"), ("3d-mvc", "MVC"),
("4K", None), ("Director's Cut", None), ("", None), (None, None),
])
def test_normalize_3d_label(label, expected):
assert normalize_3d_label(label) == expected
class TestMediaFilename:
def test_no_label_is_the_bare_stem(self):
assert media_filename("Matrix (1999)", "", ".mkv", grouped=False) == \
"Matrix (1999).mkv"
def test_3d_label_uses_jellyfins_documented_form(self):
assert media_filename("Avatar (2009)", "3D HSBS", ".mkv", grouped=False) == \
"Avatar (2009).3D.HSBS.mkv"
# same spelling grouped — "." is a valid separator after the folder name
assert media_filename("Avatar (2009)", "hsbs", ".mkv", grouped=True) == \
"Avatar (2009).3D.HSBS.mkv"
def test_plain_label_uses_dash_form(self):
assert media_filename("Avatar (2009)", "4K", ".mkv", grouped=True) == \
"Avatar (2009) - 4K.mkv"
def test_grouped_empty_label_gets_a_default(self):
assert media_filename("Avatar (2009)", "", ".mkv", grouped=True) == \
f"Avatar (2009) - {DEFAULT_VERSION_LABEL}.mkv"
class TestIdentityKey:
def test_tmdb_id_wins(self):
assert identity_key(603, "Matrix", "1999") == "tmdb-603"
assert identity_key("603", "cokoli", None) == "tmdb-603"
def test_slug_fallback_without_id(self):
assert identity_key(None, "Matrix", "1999") == "name-matrix-1999"
def test_slug_folds_diacritics(self):
assert identity_key(None, "Pelíšky", "1999") == "name-pelisky-1999"
def test_slug_without_year(self):
assert identity_key(None, "Nějaký film", None) == "name-nejaky-film"
def _movie(**kwargs) -> Movie:
base = dict(key="tmdb-19995", title="Avatar", year="2009", tmdb_id=19995,
library="Filmy")
base.update(kwargs)
return Movie(**base)
class TestMovieShape:
def test_stem_is_the_canonical_form(self):
assert _movie().stem == "Avatar (2009) [tmdbid-19995]"
def test_stem_falls_back_to_title_without_year(self):
assert _movie(year=None, tmdb_id=None).stem == "Avatar"
def test_single_video_is_not_grouped(self):
m = _movie(media=[MediaFile(Path("/pool/Filmy/x.mkv"))])
assert m.is_grouped is False
def test_two_videos_are_grouped(self):
m = _movie(media=[MediaFile(Path("/a.mkv"), "3D HSBS"),
MediaFile(Path("/b.mkv"), "Výchozí")])
assert m.is_grouped is True
def test_primary_prefers_the_plain_version(self):
m = _movie(media=[MediaFile(Path("/a.mkv"), "3D HSBS"),
MediaFile(Path("/b.mkv"), "")])
assert m.primary.label == ""
class TestDesiredLayout:
def test_one_video_stays_flat_in_the_library(self, tmp_path):
m = _movie(media=[MediaFile(tmp_path / "kdekoli.mkv", "")])
(media, target), = m.desired_layout(tmp_path)
assert target == tmp_path / "Filmy" / "Avatar (2009) [tmdbid-19995].mkv"
def test_one_3d_video_keeps_the_3d_tag_flat(self, tmp_path):
m = _movie(media=[MediaFile(tmp_path / "x.mkv", "3D HSBS")])
(_m, target), = m.desired_layout(tmp_path)
assert target.name == "Avatar (2009) [tmdbid-19995].3D.HSBS.mkv"
assert target.parent == tmp_path / "Filmy"
def test_two_videos_move_into_the_movie_folder(self, tmp_path):
m = _movie(media=[MediaFile(tmp_path / "a.mkv", ""),
MediaFile(tmp_path / "b.mkv", "3D HSBS")])
targets = {mf.label: path for mf, path in m.desired_layout(tmp_path)}
folder = tmp_path / "Filmy" / "Avatar (2009) [tmdbid-19995]"
assert targets[""] == folder / \
f"Avatar (2009) [tmdbid-19995] - {DEFAULT_VERSION_LABEL}.mkv"
assert targets["3D HSBS"] == folder / "Avatar (2009) [tmdbid-19995].3D.HSBS.mkv"
def test_layout_follows_a_retitle(self, tmp_path):
"""Renaming the movie is just a different desired layout — the thing
that used to be impossible for a grouped movie."""
m = _movie(title="Avatar: Nová verze",
media=[MediaFile(tmp_path / "a.mkv", "A"),
MediaFile(tmp_path / "b.mkv", "B")])
folders = {path.parent for _mf, path in m.desired_layout(tmp_path)}
assert folders == {
tmp_path / "Filmy" / "Avatar: Nová verze (2009) [tmdbid-19995]"}
def test_layout_follows_a_library_move(self, tmp_path):
m = _movie(library="Kids", media=[MediaFile(tmp_path / "a.mkv", "")])
(_m, target), = m.desired_layout(tmp_path)
assert target.parent == tmp_path / "Kids"
class TestRecordRoundTrip:
def test_to_and_from_record(self, tmp_path):
m = _movie(
media=[MediaFile(tmp_path / "Filmy" / "a.mkv", "3D HSBS")],
csfd_url="https://www.csfd.cz/film/1/", date="2026-01-01",
attributes={"collection_sort": "01"}, meta={"year": "2009"})
record = m.to_record(tmp_path)
assert record["media"] == [
{"role": "video", "label": "3D HSBS", "file": "Filmy/a.mkv"}]
back = Movie.from_record("tmdb-19995", record, tmp_path)
assert back.title == "Avatar"
assert back.year == "2009"
assert back.tmdb_id == 19995
assert back.library == "Filmy"
assert back.csfd_url == "https://www.csfd.cz/film/1/"
assert back.attributes == {"collection_sort": "01"}
assert back.meta == {"year": "2009"}
assert back.media[0].path == tmp_path / "Filmy" / "a.mkv"
assert back.media[0].format_3d == "HSBS"
def test_from_record_builds_tags_via_tagmanager(self, tmp_path):
from src.core.tag_manager import TagManager
record = {"title": "Avatar", "media": [], "tags": ["Žánr/Sci-Fi"]}
back = Movie.from_record("tmdb-19995", record, tmp_path, TagManager())
assert {t.full_path for t in back.tags} == {"Žánr/Sci-Fi"}