/
/
/
1"""Tests for ICY metadata parsing when a provider owns stream_metadata via callback."""
2
3from __future__ import annotations
4
5from unittest.mock import AsyncMock, MagicMock
6
7from music_assistant_models.enums import ContentType, MediaType, StreamType
8from music_assistant_models.media_items import AudioFormat
9from music_assistant_models.streamdetails import StreamDetails
10
11from music_assistant.controllers.streams.audio import StreamsAudio
12from music_assistant.controllers.streams.constants import (
13 STREAMDETAILS_INBAND_TITLE_HANDOFF_KEY,
14 STREAMDETAILS_INBAND_TITLE_KEY,
15)
16
17_META = b"StreamTitle='Some Artist - Some Song';"
18
19
20def _stream_details(*, callback: bool, opt_in: bool = False) -> StreamDetails:
21 """Return radio StreamDetails with optional callback and metadata handoff."""
22 data = {STREAMDETAILS_INBAND_TITLE_HANDOFF_KEY: True} if opt_in else None
23 return StreamDetails(
24 item_id="radio1",
25 provider="test",
26 audio_format=AudioFormat(content_type=ContentType.MP3, channels=2),
27 media_type=MediaType.RADIO,
28 stream_type=StreamType.ICY,
29 path="http://example.invalid/stream",
30 duration=0,
31 data=data,
32 stream_metadata_update_callback=AsyncMock() if callback else None,
33 )
34
35
36def test_opted_in_provider_callback_owns_stream_metadata() -> None:
37 """An opted-in callback provider receives the title through StreamDetails.data."""
38 audio = StreamsAudio(MagicMock())
39 sd = _stream_details(callback=True, opt_in=True)
40
41 audio._parse_icy_metadata(_META, sd)
42
43 assert sd.data is not None
44 assert sd.data[STREAMDETAILS_INBAND_TITLE_KEY] == "Some Artist - Some Song"
45 assert sd.stream_metadata is None
46 assert sd.stream_title is None
47
48
49def test_callback_without_opt_in_keeps_legacy_icy_metadata() -> None:
50 """A callback alone does not suppress the core ICY metadata path."""
51 mass = MagicMock()
52 mass.metadata.normalize_radio_artist_name.side_effect = lambda artist: artist
53 mass.metadata.get_radio_stream_station_image.return_value = None
54 audio = StreamsAudio(mass)
55 sd = _stream_details(callback=True)
56
57 audio._parse_icy_metadata(_META, sd)
58
59 assert sd.data is None
60 assert sd.stream_metadata is not None
61 assert sd.stream_metadata.title == "Some Song"
62 assert sd.stream_metadata.artist == "Some Artist"
63 assert sd.stream_title == "Some Artist - Some Song"
64
65
66def test_without_callback_icy_builds_stream_metadata() -> None:
67 """Without a provider callback, ICY parsing keeps building stream_metadata itself."""
68 mass = MagicMock()
69 mass.metadata.normalize_radio_artist_name.side_effect = lambda artist: artist
70 mass.metadata.get_radio_stream_station_image.return_value = None
71 audio = StreamsAudio(mass)
72 sd = _stream_details(callback=False)
73
74 audio._parse_icy_metadata(_META, sd)
75
76 assert sd.stream_metadata is not None
77 assert sd.stream_metadata.title == "Some Song"
78 assert sd.stream_metadata.artist == "Some Artist"
79 assert sd.stream_title == "Some Artist - Some Song"
80