@@ -1962,6 +1962,54 @@ async def test_put_collections_art_field_triggers_artwork_sync(patched_app, monk
19621962 mock_artwork .assert_awaited_once ()
19631963
19641964
1965+ # ── build_index — *.channel.json and *.info.json separation ──────────────────
1966+
1967+
1968+ def test_build_index_ignores_channel_json (tmp_path ):
1969+ """*.channel.json files are not indexed as videos."""
1970+ from app import build_index
1971+
1972+ # A .channel.json file that would collide if incorrectly parsed
1973+ (tmp_path / "Studio Bruxelles.channel.json" ).write_text (
1974+ json .dumps ({"id" : "should_not_be_indexed" , "channel" : "Studio Bruxelles" }),
1975+ encoding = "utf-8" ,
1976+ )
1977+ # A legitimate video info file
1978+ valid_id = "validIDxxxxx"
1979+ (tmp_path / f"Video [{ valid_id } ].info.json" ).write_text (
1980+ json .dumps ({"id" : valid_id , "title" : "Test" }),
1981+ encoding = "utf-8" ,
1982+ )
1983+
1984+ index , _ = build_index (str (tmp_path ))
1985+
1986+ assert valid_id in index , "valid info.json should be indexed"
1987+ assert "should_not_be_indexed" not in index , "channel.json must not be indexed as a video"
1988+ assert len (index ) == 1
1989+
1990+
1991+ def test_build_index_channel_json_in_subdir (tmp_path ):
1992+ """*.channel.json inside a channel subdirectory is also ignored."""
1993+ from app import build_index
1994+
1995+ channel_dir = tmp_path / "alt-J"
1996+ channel_dir .mkdir ()
1997+ (channel_dir / "alt-J.channel.json" ).write_text (
1998+ json .dumps ({"id" : "fake" , "channel" : "alt-J" }),
1999+ encoding = "utf-8" ,
2000+ )
2001+ valid_id = "altJvideo1234"
2002+ (channel_dir / f"Live Set [{ valid_id } ].info.json" ).write_text (
2003+ json .dumps ({"id" : valid_id , "title" : "Live Set" }),
2004+ encoding = "utf-8" ,
2005+ )
2006+
2007+ index , _ = build_index (str (tmp_path ))
2008+
2009+ assert valid_id in index
2010+ assert "fake" not in index
2011+
2012+
19652013# ── _fetch_channel_art ────────────────────────────────────────────────────────
19662014
19672015
@@ -2001,6 +2049,94 @@ def test_fetch_channel_art_exception_returns_none(monkeypatch):
20012049 assert result is None
20022050
20032051
2052+ def _make_yt_dlp_mock (monkeypatch , info : dict ):
2053+ """Wire up a mock yt-dlp module that returns `info` from extract_info."""
2054+ mock_ydl = MagicMock ()
2055+ mock_ydl .__enter__ = MagicMock (return_value = mock_ydl )
2056+ mock_ydl .__exit__ = MagicMock (return_value = False )
2057+ mock_ydl .extract_info .return_value = info
2058+ mock_module = MagicMock ()
2059+ mock_module .YoutubeDL .return_value = mock_ydl
2060+ monkeypatch .setattr (yamp_app , "_yt_dlp" , mock_module )
2061+ monkeypatch .setattr (yamp_app , "_YT_DLP_AVAILABLE" , True )
2062+ return mock_ydl
2063+
2064+
2065+ def test_fetch_channel_art_saves_json_flat (tmp_path , monkeypatch ):
2066+ """When no channel subdirectory exists, channel.json is saved at the data root."""
2067+ from app import _fetch_channel_art
2068+
2069+ _make_yt_dlp_mock (monkeypatch , {"channel" : "Studio Bruxelles" , "thumbnail" : "https://img/av.jpg" })
2070+
2071+ result = _fetch_channel_art ("https://www.youtube.com/@StudioBruxelles" , data_path = str (tmp_path ))
2072+
2073+ assert result is not None
2074+ assert result ["channel" ] == "Studio Bruxelles"
2075+ saved = tmp_path / "Studio Bruxelles.channel.json"
2076+ assert saved .exists (), "channel.json should be written at the data root"
2077+ data = json .loads (saved .read_text (encoding = "utf-8" ))
2078+ assert data ["channel" ] == "Studio Bruxelles"
2079+
2080+
2081+ def test_fetch_channel_art_saves_json_in_channel_dir (tmp_path , monkeypatch ):
2082+ """When a matching channel subdirectory exists, channel.json is saved inside it."""
2083+ from app import _fetch_channel_art
2084+
2085+ channel_dir = tmp_path / "Studio Bruxelles"
2086+ channel_dir .mkdir ()
2087+ _make_yt_dlp_mock (monkeypatch , {"channel" : "Studio Bruxelles" , "thumbnail" : "https://img/av.jpg" })
2088+
2089+ _fetch_channel_art ("https://www.youtube.com/@StudioBruxelles" , data_path = str (tmp_path ))
2090+
2091+ saved = channel_dir / "Studio Bruxelles.channel.json"
2092+ assert saved .exists (), "channel.json should be saved inside the existing channel dir"
2093+ assert (tmp_path / "Studio Bruxelles.channel.json" ).exists () is False , "should not also save at root"
2094+
2095+
2096+ def test_fetch_channel_art_sanitizes_filename (tmp_path , monkeypatch ):
2097+ """Channel names with unsafe characters are sanitized in the filename."""
2098+ from app import _fetch_channel_art
2099+
2100+ _make_yt_dlp_mock (monkeypatch , {"channel" : 'AC/DC: Rock"n"Roll' , "thumbnail" : "" })
2101+
2102+ _fetch_channel_art ("https://www.youtube.com/@ACDC" , data_path = str (tmp_path ))
2103+
2104+ files = list (tmp_path .iterdir ())
2105+ assert len (files ) == 1
2106+ assert files [0 ].name .endswith (".channel.json" )
2107+ assert "/" not in files [0 ].name
2108+ assert ":" not in files [0 ].name
2109+
2110+
2111+ def test_fetch_channel_art_save_error_does_not_raise (tmp_path , monkeypatch ):
2112+ """An OSError while saving channel.json is logged but does not affect the return value."""
2113+ from app import _fetch_channel_art
2114+
2115+ _make_yt_dlp_mock (monkeypatch , {"channel" : "Test Channel" , "thumbnail" : "https://img/av.jpg" })
2116+
2117+ # Make tmp_path read-only so the write fails
2118+ tmp_path .chmod (0o555 )
2119+ try :
2120+ result = _fetch_channel_art ("https://www.youtube.com/@TestChannel" , data_path = str (tmp_path ))
2121+ finally :
2122+ tmp_path .chmod (0o755 ) # restore so tmp_path cleanup works
2123+
2124+ assert result is not None
2125+ assert result ["channel" ] == "Test Channel"
2126+
2127+
2128+ def test_fetch_channel_art_no_data_path_skips_save (tmp_path , monkeypatch ):
2129+ """When data_path is None, no file is written."""
2130+ from app import _fetch_channel_art
2131+
2132+ _make_yt_dlp_mock (monkeypatch , {"channel" : "Test Channel" , "thumbnail" : "https://img/av.jpg" })
2133+
2134+ result = _fetch_channel_art ("https://www.youtube.com/@TestChannel" , data_path = None )
2135+
2136+ assert result is not None
2137+ assert list (tmp_path .iterdir ()) == [], "no file should be written when data_path is None"
2138+
2139+
20042140# ── _prefetch_channel_art_bg — error handling ────────────────────────────────
20052141
20062142
0 commit comments