/
/
/
1"""Tests for parsing YTM podcasts from browse/search results."""
2
3from unittest.mock import MagicMock
4
5from music_assistant_models.media_items import Podcast
6from ytmusicapi.parsers.podcasts import Description
7
8from music_assistant.providers.ytmusic import YoutubeMusicProvider
9
10
11def _make_provider() -> MagicMock:
12 """Create a mock YoutubeMusicProvider with the fields used by the parsers."""
13 prov = MagicMock()
14 prov.instance_id = "ytmusic_test"
15 prov.domain = "ytmusic"
16 prov._parse_thumbnails = MagicMock(return_value=[])
17 prov._parse_podcast = lambda obj: YoutubeMusicProvider._parse_podcast(prov, obj)
18 return prov
19
20
21def _parse_episode(description: object) -> str | None:
22 """Parse an episode with the given raw description and return the parsed one."""
23 prov = _make_provider()
24 podcast = Podcast(
25 item_id="PLtest123", provider="ytmusic_test", name="Couple Of", provider_mappings=set()
26 )
27 episode_obj = {"videoId": "vid123", "title": "Episode 1", "description": description}
28
29 episode = YoutubeMusicProvider._parse_podcast_episode(prov, episode_obj, podcast)
30
31 return episode.metadata.description
32
33
34def test_browse_podcast_with_mpsp_browse_id_is_parsed() -> None:
35 """A home/search item with an MPSP browseId must parse into a Podcast."""
36 prov = _make_provider()
37 item = {
38 "title": "Couple Of",
39 "browseId": "MPSPPLtest123",
40 "podcastId": None,
41 "channel": {"id": "UC123", "name": "Some Channel"},
42 }
43
44 podcast = YoutubeMusicProvider._parse_browse_podcast(prov, item)
45
46 assert isinstance(podcast, Podcast)
47 assert podcast.item_id == "PLtest123"
48 assert podcast.name == "Couple Of"
49 assert podcast.publisher == "Some Channel"
50
51
52def test_browse_podcast_prefers_explicit_podcast_id() -> None:
53 """The item's podcastId must be preferred over the derived browseId."""
54 prov = _make_provider()
55 item = {
56 "title": "Couple Of",
57 "browseId": "MPSPPLtest123",
58 "podcastId": "PLexplicit456",
59 }
60
61 podcast = YoutubeMusicProvider._parse_browse_podcast(prov, item)
62
63 assert isinstance(podcast, Podcast)
64 assert podcast.item_id == "PLexplicit456"
65
66
67def test_browse_album_is_not_parsed_as_podcast() -> None:
68 """An album item (MPRE browseId) must not be parsed as a podcast."""
69 prov = _make_provider()
70 item = {"title": "Some Album", "browseId": "MPREb_test123"}
71
72 assert YoutubeMusicProvider._parse_browse_podcast(prov, item) is None
73
74
75def test_browse_item_without_browse_id_is_not_parsed_as_podcast() -> None:
76 """An item without browseId must not be parsed as a podcast."""
77 prov = _make_provider()
78
79 assert YoutubeMusicProvider._parse_browse_podcast(prov, {"title": "X"}) is None
80
81
82def test_episode_description_object_is_stored_as_text() -> None:
83 """A Description object must be flattened, as the metadata expects a string."""
84 description = Description.from_runs([{"text": "Part one. "}, {"text": "Part two."}])
85
86 assert _parse_episode(description) == "Part one. Part two."
87
88
89def test_episode_description_string_is_stored_as_is() -> None:
90 """A description that already is a string must be left untouched."""
91 assert _parse_episode("Part one. Part two.") == "Part one. Part two."
92