/
/
/
1"""Tests for the synopsis to description mapping of the BBC Sounds converters."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6
7import pytest
8from music_assistant_models.media_items import PodcastEpisode as MAPodcastEpisode
9from sounds.models import Podcast, PodcastEpisode, RadioClip, RadioShow
10
11from music_assistant.providers.bbc_sounds.adaptor import Context, PodcastConverter
12from music_assistant.providers.bbc_sounds.constants import _Constants
13
14if TYPE_CHECKING:
15 from music_assistant.providers.bbc_sounds import BBCSoundsProvider
16
17SYNOPSES = {
18 "short": "Short synopsis",
19 "medium": "Medium synopsis",
20 "long": "Long synopsis",
21}
22
23
24@pytest.fixture
25def converter(provider: BBCSoundsProvider) -> PodcastConverter:
26 """Create a PodcastConverter bound to the test provider."""
27 return PodcastConverter(Context(provider=provider, provider_domain="bbc_sounds"))
28
29
30def _podcast() -> Podcast:
31 return Podcast(id="p01", titles={"primary": "A podcast"}, synopses=SYNOPSES)
32
33
34@pytest.mark.parametrize(
35 ("synopses", "expected"),
36 [
37 (SYNOPSES, "Long synopsis"),
38 ({"short": "Short synopsis", "medium": "Medium synopsis"}, "Medium synopsis"),
39 ({"short": "Short synopsis"}, "Short synopsis"),
40 ({}, None),
41 ],
42)
43def test_get_synopsis_prefers_the_longest_available(
44 converter: PodcastConverter, synopses: dict[str, str], expected: str | None
45) -> None:
46 """The fullest synopsis wins, and nothing is returned when there is none."""
47 assert converter._get_synopsis(Podcast(id="p01", synopses=synopses)) == expected
48
49
50async def test_podcast_episode_gets_the_long_synopsis(converter: PodcastConverter) -> None:
51 """A podcast episode description comes from the long synopsis."""
52 episode = PodcastEpisode(
53 id="e01",
54 pid="e01",
55 titles={"primary": "A podcast", "secondary": "An episode"},
56 synopses=SYNOPSES,
57 container=_podcast(),
58 )
59
60 result = await converter.convert(episode)
61
62 assert isinstance(result, MAPodcastEpisode)
63 assert result.metadata.description == "Long synopsis"
64
65
66async def test_radio_show_episode_gets_a_description(converter: PodcastConverter) -> None:
67 """A radio show served as an episode carries its synopsis as the description."""
68 show = RadioShow(
69 id="s01",
70 pid="s01",
71 duration={"value": _Constants.TRACK_DURATION_THRESHOLD + 1},
72 titles={"primary": "A show", "secondary": "An edition"},
73 synopses=SYNOPSES,
74 container=_podcast(),
75 )
76
77 result = await converter.convert(show)
78
79 assert isinstance(result, MAPodcastEpisode)
80 assert result.metadata.description == "Long synopsis"
81
82
83async def test_stream_title_is_the_episode_name(converter: PodcastConverter) -> None:
84 """The now playing title is the episode name, never its synopsis."""
85 episode = PodcastEpisode(
86 id="e01",
87 pid="e01",
88 titles={"primary": "A podcast", "secondary": "An episode"},
89 synopses=SYNOPSES,
90 container=_podcast(),
91 stream="https://example.invalid/e01.m3u8",
92 )
93
94 converter.context.provider.stream_format = _Constants.HLS
95
96 stream_details = await converter.get_stream_details(episode)
97
98 assert stream_details is not None
99 assert stream_details.stream_metadata is not None
100 assert stream_details.stream_metadata.title == "An episode"
101
102
103async def test_radio_clip_description_is_a_synopsis(converter: PodcastConverter) -> None:
104 """A clip description is its synopsis rather than the name of its network."""
105 clip = RadioClip(
106 id="c01",
107 pid="c01",
108 duration={"value": 60},
109 titles={"entity_title": "A clip"},
110 synopses=SYNOPSES,
111 )
112
113 result = await converter.convert(clip)
114
115 assert result.metadata.description == "Long synopsis"
116
117
118async def test_clip_stream_title_survives_titles_without_a_primary_key(
119 converter: PodcastConverter,
120) -> None:
121 """A clip keys its titles on entity_title, so the primary lookup must not raise."""
122 clip = RadioClip(
123 id="c01",
124 pid="c01",
125 duration={"value": 60},
126 titles={"entity_title": "A clip"},
127 synopses=SYNOPSES,
128 stream="https://example.invalid/c01.m3u8",
129 )
130
131 converter.context.provider.stream_format = _Constants.HLS
132
133 stream_details = await converter.get_stream_details(clip)
134
135 assert stream_details is not None
136 assert stream_details.stream_metadata is not None
137 assert stream_details.stream_metadata.title == ""
138