from datetime import date import pytest from youtube_automate import naming class TestSanitise: @pytest.mark.parametrize( "raw, expected", [ ("Hermitcraft S11#11: Expanding Business", "Hermitcraft S11#11 Expanding Business"), ("A/B", "A B"), ('Say "hello" ', "Say hello now"), ("path\\to\\thing", "path to thing"), ("what? really * | yes", "what really yes"), (" .leading and trailing. ", "leading and trailing"), ("line one\nline two", "line one line two"), ("tabs\tand\r\nnewlines", "tabs and newlines"), ], ) def test_removes_illegal_characters(self, raw, expected): assert naming.sanitize_component(raw) == expected def test_empty_input_is_empty(self): assert naming.sanitize_component("") == "" assert naming.sanitize_component(None) == "" def test_title_that_is_only_illegal_characters_collapses_to_empty(self): assert naming.sanitize_component("///???") == "" def test_truncates_to_120_characters(self): long = "word " * 60 result = naming.sanitize_component(long) assert len(result) <= naming.MAX_TITLE_LEN def test_truncation_prefers_a_word_boundary(self): text = "alpha bravo charlie delta echo foxtrot golf hotel india juliet " * 3 result = naming.sanitize_component(text) assert not result.endswith(" ") # Should not cut mid-word when a boundary is available late enough. assert result == result.rstrip() assert " " in result def test_hard_cuts_when_no_late_word_boundary_exists(self): text = "a" + "b" * 400 result = naming.sanitize_component(text) assert len(result) == naming.MAX_TITLE_LEN class TestEpisodeNumbering: @pytest.mark.parametrize( "day, ordinal, expected", [ (date(2026, 8, 11), 0, 8110), (date(2026, 8, 11), 1, 8111), (date(2026, 8, 12), 0, 8120), (date(2026, 1, 1), 0, 1010), (date(2026, 12, 31), 0, 12310), (date(2026, 12, 31), 9, 12319), ], ) def test_episode_number(self, day, ordinal, expected): assert naming.episode_number(day, ordinal) == expected def test_ordinal_is_clamped_at_nine(self): assert naming.episode_number(date(2026, 8, 11), 12) == 8119 def test_negative_ordinal_clamps_to_zero(self): assert naming.episode_number(date(2026, 8, 11), -3) == 8110 def test_numbers_sort_chronologically_across_the_year(self): days = [date(2026, 1, 1), date(2026, 6, 15), date(2026, 8, 11), date(2026, 12, 31)] numbers = [naming.episode_number(day, 0) for day in days] assert numbers == sorted(numbers) def test_episode_range_covers_ten_slots(self): low, high = naming.episode_range(date(2026, 8, 11)) assert (low, high) == (8110, 8119) def test_season_is_the_upload_year(self): assert naming.season_for(date(2026, 8, 11)) == 2026 class TestParseUploadDate: def test_accepts_ytdlp_compact_form(self): assert naming.parse_upload_date("20260811") == date(2026, 8, 11) def test_accepts_iso_form(self): assert naming.parse_upload_date("2026-08-11") == date(2026, 8, 11) def test_accepts_iso_timestamp(self): assert naming.parse_upload_date("2026-08-11T12:00:00+00:00") == date(2026, 8, 11) def test_passes_through_a_date(self): assert naming.parse_upload_date(date(2026, 8, 11)) == date(2026, 8, 11) def test_rejects_nonsense(self): with pytest.raises(ValueError): naming.parse_upload_date("not a date") class TestBasename: def test_includes_video_id_for_uniqueness(self): stem = naming.basename("clabretro", 2026, 8110, "A Title", "dQw4w9WgXcQ") assert stem == "clabretro - S2026E8110 - A Title [dQw4w9WgXcQ]" def test_episode_is_unpadded_to_match_the_nfo(self): stem = naming.basename("c", 2026, 8110, "t", "id") assert "S2026E8110" in stem assert "E08110" not in stem def test_falls_back_to_video_id_when_the_title_sanitises_away(self): stem = naming.basename("c", 2026, 8110, "///", "dQw4w9WgXcQ") assert stem.endswith("dQw4w9WgXcQ [dQw4w9WgXcQ]") def test_two_videos_same_title_differ_by_id(self): one = naming.basename("c", 2026, 8110, "Same", "aaaaaaaaaaa") two = naming.basename("c", 2026, 8111, "Same", "bbbbbbbbbbb") assert one != two class TestChannelDirName: def test_sanitises_the_title(self): assert naming.channel_dir_name("Tom / Jerry", "UCabc") == "Tom Jerry" def test_falls_back_to_channel_id_when_title_is_unusable(self): assert naming.channel_dir_name("???", "UCabc") == "UCabc"