"""Tests for date-based ("daily"/sports) episode naming via Episode.air_date.""" from __future__ import annotations from datetime import date, datetime import pytest from unshackle.core.config import config from unshackle.core.titles.episode import Episode 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=2024, number=181, name="The Big Race", year=2024, ) kwargs.update(overrides) return Episode(**kwargs) @pytest.fixture def reset_template(): saved = (config.output_template, config.folder_templates, config.folder_template) config.output_template = {"series": "{title}.{year?}.{season_episode}.{quality}-{tag}"} config.folder_templates = {} config.folder_template = "" yield config config.output_template, config.folder_templates, config.folder_template = saved def test_air_date_replaces_season_episode_dotted(reset_template): ep = make_episode(air_date=date(2024, 6, 30)) ctx = ep.build_template_context(StubMediaInfo()) assert ctx["season_episode"] == "2024.06.30" assert ctx["season"] == "2024.06.30" assert ctx["episode"] == "" assert ctx["year"] == "" # date carries the year assert ctx["date"] == "2024-06-30" # ISO regardless of separator def test_air_date_uses_space_separator(reset_template): reset_template.output_template = {"series": "{title} {season_episode} {quality}-{tag}"} ep = make_episode(air_date=date(2024, 6, 30)) assert ep.build_template_context(StubMediaInfo())["season_episode"] == "2024 06 30" def test_iso_string_air_date_coerced(reset_template): ep = make_episode(air_date="2024-06-30") assert isinstance(ep.air_date, date) assert ep.build_template_context(StubMediaInfo())["season_episode"] == "2024.06.30" def test_year_always_dropped_in_file_when_dated(reset_template): """The air date is the sole date in the filename; a distinct year is dropped too (deterministic).""" ep = make_episode(year=2010, air_date=date(2013, 10, 30)) ctx = ep.build_template_context(StubMediaInfo()) assert ctx["year"] == "" assert ctx["season_episode"] == "2013.10.30" def test_yearless_title_takes_year_from_date(reset_template): """A title with no year still names cleanly; the date supplies the year.""" ep = make_episode(year=None, air_date=date(2026, 6, 28)) ctx = ep.build_template_context(StubMediaInfo()) assert ctx["year"] == "" assert ctx["season_episode"] == "2026.06.28" def test_datetime_normalized_to_date(reset_template): """A datetime (subclass of date) must not leak its time into the {date} token.""" ep = make_episode(air_date=datetime(2024, 6, 30, 13, 0, 0)) assert type(ep.air_date) is date assert ep.build_template_context(StubMediaInfo())["date"] == "2024-06-30" def test_no_air_date_is_unchanged(reset_template): """Regression: without air_date, naming is exactly as before (presentation-only field).""" ep = make_episode() ctx = ep.build_template_context(StubMediaInfo()) assert ctx["season_episode"] == "S2024E181" assert ctx["season"] == "S2024" assert ctx["episode"] == "E181" assert ctx["year"] == 2024 assert ctx["date"] == "" def test_dated_folder_groups_by_year(reset_template): reset_template.folder_templates = {} reset_template.folder_template = "" folder = make_episode(air_date=date(2024, 6, 30)).get_filename(StubMediaInfo(), folder=True) assert "2024" in folder assert "S2024" not in folder # season folder is the year, not the faked season def test_dated_folder_drops_year_matching_air_year(reset_template): """{year?} would repeat the air-year season label (Title.2024.2024); it must render once.""" folder = make_episode(air_date=date(2024, 6, 30)).get_filename(StubMediaInfo(), folder=True) assert "2024.2024" not in folder def test_dated_folder_keeps_distinct_year(reset_template): """A series year different from the air year is real information and stays.""" folder = make_episode(year=2010, air_date=date(2013, 10, 30)).get_filename(StubMediaInfo(), folder=True) assert "2010" in folder assert "2013" in folder def test_a_dated_episode_matches_its_iso_date_key(): assert make_episode(air_date=date(2024, 6, 30)).matches_wanted({"2024-06-30"}) def test_a_dated_episode_still_matches_its_season_episode_key(): assert make_episode(air_date=date(2024, 6, 30)).matches_wanted({"2024x181"}) def test_a_date_key_does_not_match_another_day(): assert not make_episode(air_date=date(2024, 6, 30)).matches_wanted({"2024-07-01"}) def test_a_season_episode_exclusion_beats_the_date_key(): assert not make_episode(air_date=date(2024, 6, 30)).matches_wanted({"2024-06-30", "!2024x181"}) def test_an_undated_episode_is_unaffected_by_date_keys(): episode = make_episode() assert not episode.matches_wanted({"2024-06-30"}) assert episode.matches_wanted({"2024x181"}) def test_a_pre_1970_air_date_is_rejected(): with pytest.raises(ValueError): make_episode(air_date=date(1969, 12, 31)) def test_a_future_air_date_is_accepted(): future = date.today().replace(year=date.today().year + 1) assert make_episode(air_date=future).air_date == future def test_an_unparseable_air_date_string_is_kept_as_is(): assert make_episode(air_date="soon").air_date == "soon"