/
/
/
1"""Tests for the Squeezelite player."""
2
3from __future__ import annotations
4
5from collections.abc import AsyncIterator
6from unittest.mock import MagicMock
7
8import pytest
9from music_assistant_models.enums import MediaType
10from music_assistant_models.player import PlayerMedia
11
12from music_assistant.constants import (
13 CONF_ENTRY_PREFER_WAV_FOR_LIVE_SOURCES_DEFAULT_ENABLED,
14 CONF_OUTPUT_CODEC,
15 CONF_PREFER_WAV_FOR_LIVE_SOURCES,
16)
17from music_assistant.providers.squeezelite.player import SqueezelitePlayer, is_protocol_only_device
18
19
20async def test_squeezelite_prefers_wav_for_live_sources_by_default() -> None:
21 """Squeezelite players default to the known-compatible low-latency WAV path."""
22
23 async def _no_library_items(*_args: object, **_kwargs: object) -> AsyncIterator[object]:
24 return
25 yield # pragma: no cover - makes this an async generator
26
27 player = SqueezelitePlayer.__new__(SqueezelitePlayer)
28 player.mass = mass = MagicMock()
29 mass.music.playlists.iter_library_items = _no_library_items
30 mass.music.radio.iter_library_items = _no_library_items
31
32 entries = await player.get_config_entries()
33 entry = next(entry for entry in entries if entry.key == CONF_PREFER_WAV_FOR_LIVE_SOURCES)
34
35 assert (
36 entry.default_value == CONF_ENTRY_PREFER_WAV_FOR_LIVE_SOURCES_DEFAULT_ENABLED.default_value
37 )
38
39
40def _member_player(*, prefer_wav_for_live_sources: bool) -> MagicMock:
41 """Return a mocked member player with the given effective WAV preference."""
42 member = MagicMock()
43 member.config.get_value.side_effect = lambda key, default=None: (
44 prefer_wav_for_live_sources if key == CONF_PREFER_WAV_FOR_LIVE_SOURCES else default
45 )
46 return member
47
48
49def _player_with_mocked_mass() -> tuple[SqueezelitePlayer, MagicMock]:
50 """Return an uninitialized SqueezelitePlayer alongside its mocked mass instance."""
51 player = SqueezelitePlayer.__new__(SqueezelitePlayer)
52 player.mass = mass = MagicMock()
53 return player, mass
54
55
56def test_group_audio_source_uses_wav_when_member_prefers_it() -> None:
57 """A sync group member that opted into low-latency WAV gets WAV for AudioSource media."""
58 player, mass = _player_with_mocked_mass()
59 mass.players.get_player.return_value = _member_player(prefer_wav_for_live_sources=True)
60
61 codec = player._get_member_output_codec(
62 "member_1", PlayerMedia(uri="fake://x", media_type=MediaType.AUDIO_SOURCE)
63 )
64
65 assert codec == "wav"
66 mass.config.get_raw_player_config_value.assert_not_called()
67
68
69def test_group_audio_source_falls_back_to_output_codec_when_disabled() -> None:
70 """A sync group member that disabled the WAV preference uses its configured output codec."""
71 player, mass = _player_with_mocked_mass()
72 mass.players.get_player.return_value = _member_player(prefer_wav_for_live_sources=False)
73 mass.config.get_raw_player_config_value.return_value = "aac"
74
75 codec = player._get_member_output_codec(
76 "member_1", PlayerMedia(uri="fake://x", media_type=MediaType.AUDIO_SOURCE)
77 )
78
79 assert codec == "aac"
80 mass.config.get_raw_player_config_value.assert_called_once_with(
81 "member_1", CONF_OUTPUT_CODEC, "flac"
82 )
83
84
85def test_group_regular_track_ignores_wav_preference() -> None:
86 """Regular track playback in a sync group always uses the member's configured codec."""
87 player, mass = _player_with_mocked_mass()
88 mass.config.get_raw_player_config_value.return_value = "flac"
89
90 codec = player._get_member_output_codec(
91 "member_1", PlayerMedia(uri="fake://x", media_type=MediaType.TRACK)
92 )
93
94 assert codec == "flac"
95 mass.players.get_player.assert_not_called()
96
97
98@pytest.mark.parametrize(
99 ("device_model", "expected"),
100 [
101 # generic squeezelite (software) players are full players
102 ("SqueezeLite", False),
103 ("SqueezeLite-HA-Addon", False),
104 ("SqueezePlay", False),
105 ("SqueezeESP32", False),
106 # hardware players are full players
107 ("Squeezebox Boom", False),
108 ("Transporter", False),
109 # unknown/absent model info defaults to a full player
110 ("", False),
111 ("Unknown", False),
112 # WiiM/LinkPlay devices use squeezelite as a secondary protocol
113 ("WiiM Player", True),
114 ("wiim mini", True),
115 # LMS bridge tools represent devices that are already players themselves
116 ("RaopBridge", True),
117 ("CastBridge", True),
118 ("UPnPBridge", True),
119 ],
120)
121def test_is_protocol_only_device(device_model: str, expected: bool) -> None:
122 """Test protocol-only device detection based on the reported device model."""
123 assert is_protocol_only_device(device_model) is expected
124