Files
unshackle/tests/core/test_episode_absolute.py

207 lines
7.4 KiB
Python

"""Absolute episode numbering: Episode.absolute, the {absolute} token, and the enrich fill."""
from __future__ import annotations
from types import SimpleNamespace
import pytest
from unshackle.core import providers
from unshackle.core.config import config
from unshackle.core.titles.episode import Episode, Series
class DummyService:
pass
class StubMediaInfo:
"""Minimal MediaInfo stand-in: the template context only reads track lists."""
video_tracks: list = []
audio_tracks: list = []
def make_episode(**overrides) -> Episode:
kwargs = dict(
id_="ep-0001",
service=DummyService,
title="The Show",
season=1,
number=1,
name="The Reckoning",
)
kwargs.update(overrides)
return Episode(**kwargs)
@pytest.fixture
def reset_template():
saved = (config.output_template, config.folder_templates, config.folder_template, config.tag)
config.output_template = {"series": "{title}.{season_episode}.{absolute}.{source}-{tag}"}
config.folder_templates = {}
config.folder_template = ""
config.tag = "GROUP"
yield config
config.output_template, config.folder_templates, config.folder_template, config.tag = saved
def test_absolute_defaults_to_none():
assert make_episode().absolute is None
def test_int_absolute_is_kept():
assert make_episode(absolute=27).absolute == 27
def test_digit_string_absolute_coerces():
assert make_episode(absolute="27").absolute == 27
@pytest.mark.parametrize("bad", [0, -1])
def test_non_positive_absolute_rejected(bad):
with pytest.raises(ValueError):
make_episode(absolute=bad)
@pytest.mark.parametrize("bad", ["x", "1.5", 1.5, []])
def test_non_int_absolute_rejected(bad):
with pytest.raises(TypeError):
make_episode(absolute=bad)
def test_bool_absolute_rejected():
"""bool is an int subclass; True would render as '001' in filenames."""
with pytest.raises(TypeError):
make_episode(absolute=True)
def test_old_cache_entry_reads_as_absolute_less():
"""jsonpickle restores by populating __dict__; an older entry has no 'absolute' key."""
restored = Episode.__new__(Episode)
restored.__dict__.update(title="The Show", season=1, number=1, name=None, year=None)
assert restored.absolute is None
def test_absolute_token_is_three_wide(reset_template):
assert make_episode(absolute=7).build_template_context(StubMediaInfo())["absolute"] == "007"
def test_absolute_token_does_not_truncate_past_three_digits(reset_template):
assert make_episode(absolute=1042).build_template_context(StubMediaInfo())["absolute"] == "1042"
def test_absolute_token_is_empty_without_a_value(reset_template):
assert make_episode().build_template_context(StubMediaInfo())["absolute"] == ""
def test_filename_renders_the_absolute_number(reset_template):
assert make_episode(absolute=7).get_filename(StubMediaInfo()) == "The.Show.S01E01.007.DummyService-GROUP"
def test_optional_absolute_token_drops_out(reset_template):
reset_template.output_template = {"series": "{title}.{season_episode}.{absolute?}.{source}-{tag}"}
assert make_episode().get_filename(StubMediaInfo()) == "The.Show.S01E01.DummyService-GROUP"
assert make_episode(absolute=7).get_filename(StubMediaInfo()) == "The.Show.S01E01.007.DummyService-GROUP"
def test_absolute_is_a_known_template_variable(reset_template, recwarn):
reset_template.validate_output_templates()
assert not [w for w in recwarn.list if "absolute" in str(w.message)]
def test_str_and_sorting_ignore_the_absolute_number():
assert str(make_episode(absolute=7)) == "The Show S01E01 The Reckoning"
series = Series([make_episode(number=2, absolute=1), make_episode(number=1, absolute=2)])
assert [e.number for e in series] == [1, 2]
class _FakeProvider:
def __init__(self, mapping: dict, source_order: str = "official") -> None:
self.mapping = mapping
self.source_order = source_order
self.orders: list = []
def detect_order(self, _id, _keys) -> str:
return self.source_order
def get_order_map(self, _id, order, source_order=None) -> dict:
self.orders.append(order)
return self.mapping
def fill(
monkeypatch: pytest.MonkeyPatch, episodes: list, mapping: dict, tvdb_id=73871, provider=True, order="official"
):
from unshackle.commands.dl import dl
fake = _FakeProvider(mapping, order) if provider else None
monkeypatch.setattr(providers, "get_provider", lambda _name: fake)
logged: list = []
logger = SimpleNamespace(
debug=lambda *a, **k: logged.append(("debug", a)),
info=lambda *a, **k: logged.append(("info", a)),
warning=lambda *a, **k: logged.append(("warning", a)),
)
stub = SimpleNamespace(tvdb_id=tvdb_id, log=logger)
titles = Series(episodes)
dl.fill_absolute_numbers(stub, titles, tvdb_id)
return titles, fake, logged
MAPPING = {(1, 1): (1, 1, "One"), (1, 2): (1, 2, "Two"), (2, 1): (1, 3, "Three")}
def test_fill_populates_missing_absolute_numbers(monkeypatch: pytest.MonkeyPatch) -> None:
episodes = [make_episode(season=1, number=1), make_episode(season=1, number=2), make_episode(season=2, number=1)]
titles, fake, _ = fill(monkeypatch, episodes, MAPPING)
assert [t.absolute for t in titles] == [1, 2, 3]
assert fake.orders == ["absolute"]
def test_a_service_already_in_absolute_order_fills_from_its_own_numbers(monkeypatch: pytest.MonkeyPatch) -> None:
"""get_order_map returns {} for the same-order case; the numbers are their own fill."""
episodes = [make_episode(season=1, number=26), make_episode(season=1, number=27)]
titles, fake, _ = fill(monkeypatch, episodes, {}, order="absolute")
assert [t.absolute for t in titles] == [26, 27]
assert fake.orders == []
def test_fill_never_touches_season_or_number(monkeypatch: pytest.MonkeyPatch) -> None:
episodes = [make_episode(season=2, number=1)]
titles, _, _ = fill(monkeypatch, episodes, MAPPING)
assert [(t.season, t.number) for t in titles] == [(2, 1)]
def test_a_service_set_absolute_number_wins(monkeypatch: pytest.MonkeyPatch) -> None:
episodes = [make_episode(season=1, number=1, absolute=99), make_episode(season=1, number=2)]
titles, _, _ = fill(monkeypatch, episodes, MAPPING)
assert [t.absolute for t in titles] == [99, 2]
def test_an_unmapped_episode_keeps_no_absolute_number(monkeypatch: pytest.MonkeyPatch) -> None:
episodes = [make_episode(season=9, number=9)]
titles, _, _ = fill(monkeypatch, episodes, MAPPING)
assert titles[0].absolute is None
def test_fill_skips_silently_without_an_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
episodes = [make_episode(season=1, number=1)]
titles, _, logged = fill(monkeypatch, episodes, MAPPING, provider=False)
assert titles[0].absolute is None
assert [level for level, _ in logged] == ["debug"]
def test_fill_skips_silently_without_a_tvdb_id(monkeypatch: pytest.MonkeyPatch) -> None:
episodes = [make_episode(season=1, number=1)]
titles, _, logged = fill(monkeypatch, episodes, MAPPING, tvdb_id=None)
assert titles[0].absolute is None
assert [level for level, _ in logged] == ["debug"]
def test_fill_skips_silently_when_tvdb_has_no_absolute_order(monkeypatch: pytest.MonkeyPatch) -> None:
episodes = [make_episode(season=1, number=1)]
titles, _, logged = fill(monkeypatch, episodes, {})
assert titles[0].absolute is None
assert [level for level, _ in logged] == ["debug"]