"""Channel URL resolution and the subscribe/unsubscribe lifecycle.""" import pytest from youtube_automate import channels, config class TestNormaliseUrl: @pytest.mark.parametrize( "raw, expected", [ ("https://www.youtube.com/@clabretro", "https://www.youtube.com/@clabretro"), ("http://youtube.com/c/name", "http://youtube.com/c/name"), ("@clabretro", "https://www.youtube.com/@clabretro"), ("clabretro", "https://www.youtube.com/@clabretro"), ( "UCW7jUEpYT_t0Gsf632d6_wQ", "https://www.youtube.com/channel/UCW7jUEpYT_t0Gsf632d6_wQ", ), ("www.youtube.com/@x", "https://www.youtube.com/@x"), (" @spaced ", "https://www.youtube.com/@spaced"), ], ) def test_accepted_forms(self, raw, expected): assert channels.normalise_url(raw) == expected @pytest.mark.parametrize("raw", ["", " ", "not a channel!!", "@@@"]) def test_rejected_forms(self, raw): with pytest.raises(channels.ResolutionError): channels.normalise_url(raw) def test_channel_id_must_be_the_right_shape(self): # Too short to be a real UC id, so it is treated as a handle instead. assert channels.normalise_url("UCshort") == "https://www.youtube.com/@UCshort" class TestUulfPlaylistId: def test_swaps_the_uc_prefix_for_uulf(self): assert ( channels.uulf_playlist_id("UCW7jUEpYT_t0Gsf632d6_wQ") == "UULFW7jUEpYT_t0Gsf632d6_wQ" ) def test_length_is_preserved(self): channel_id = "UCW7jUEpYT_t0Gsf632d6_wQ" assert len(channels.uulf_playlist_id(channel_id)) == len(channel_id) + 2 class TestThumbnailPicking: def test_finds_the_requested_id(self): thumbs = [ {"id": "avatar_uncropped", "url": "http://a/avatar.jpg"}, {"id": "banner_uncropped", "url": "http://a/banner.jpg"}, ] assert channels._pick_thumbnail(thumbs, "banner_uncropped") == "http://a/banner.jpg" def test_returns_none_when_absent(self): assert channels._pick_thumbnail([{"id": "other", "url": "u"}], "avatar_uncropped") is None def test_ignores_entries_without_a_url(self): assert channels._pick_thumbnail([{"id": "avatar_uncropped"}], "avatar_uncropped") is None def test_empty_list(self): assert channels._pick_thumbnail([], "avatar_uncropped") is None class TestUniqueDirName: def test_first_use_is_unchanged(self, conn): assert channels._unique_dir_name(conn, "clabretro") == "clabretro" def test_collision_gets_a_suffix(self, conn, channel): assert channels._unique_dir_name(conn, "clabretro") == "clabretro (2)" def test_repeated_collisions_keep_counting(self, conn, channel): with conn: conn.execute( "INSERT INTO channel (channel_id, title, dir_name, added_at) " "VALUES ('UCx', 'clabretro', 'clabretro (2)', '2026-01-01')" ) assert channels._unique_dir_name(conn, "clabretro") == "clabretro (3)" class TestUnsubscribe: def test_removes_the_directory_and_the_rows(self, conn, channel, media_root, monkeypatch): monkeypatch.setattr(config, "MEDIA_ROOT", media_root) channel_dir = media_root / channel["dir_name"] (channel_dir / "Season 2026").mkdir(parents=True) (channel_dir / "tvshow.nfo").write_text("") from conftest import add_video add_video(conn, channel["id"], "v1") title = channels.unsubscribe(conn, channel["id"]) assert title == "clabretro" assert not channel_dir.exists() assert conn.execute("SELECT COUNT(*) FROM channel").fetchone()[0] == 0 # ON DELETE CASCADE must take the videos with it. assert conn.execute("SELECT COUNT(*) FROM video").fetchone()[0] == 0 def test_missing_channel_raises(self, conn): with pytest.raises(LookupError): channels.unsubscribe(conn, 999) def test_tolerates_a_missing_directory(self, conn, channel, media_root, monkeypatch): monkeypatch.setattr(config, "MEDIA_ROOT", media_root) channels.unsubscribe(conn, channel["id"]) assert conn.execute("SELECT COUNT(*) FROM channel").fetchone()[0] == 0