mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
Renames 314 defs and 41 module/class-level constants. Keeps 11 names underscored where the stripped form collides in scope, plus 5.5.0 shims for the three symbols external service repos call.
216 lines
7.6 KiB
Python
216 lines
7.6 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
|
|
|
|
|
|
# --- validation -------------------------------------------------------------
|
|
|
|
|
|
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
|
|
|
|
|
|
# --- naming -----------------------------------------------------------------
|
|
|
|
|
|
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]
|
|
|
|
|
|
# --- the enrich fill --------------------------------------------------------
|
|
|
|
|
|
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"]
|