/
/
/
1"""Snapcast Player provider for Music Assistant."""
2
3import re
4
5from music_assistant_models.config_entries import (
6 ConfigEntry,
7 ConfigValueOption,
8 ConfigValueType,
9 ProviderConfig,
10)
11from music_assistant_models.enums import ConfigEntryType, ProviderFeature
12from music_assistant_models.errors import SetupFailedError
13from music_assistant_models.provider import ProviderManifest
14
15from music_assistant.helpers.process import check_output
16from music_assistant.mass import MusicAssistant
17from music_assistant.models import ProviderInstanceType
18from music_assistant.providers.snapcast.constants import (
19 CONF_CATEGORY_BUILT_IN,
20 CONF_HELP_LINK,
21 CONF_SERVER_BUFFER_SIZE,
22 CONF_SERVER_CHUNK_MS,
23 CONF_SERVER_CONTROL_PORT,
24 CONF_SERVER_HOST,
25 CONF_SERVER_INITIAL_VOLUME,
26 CONF_SERVER_SEND_AUDIO_TO_MUTED,
27 CONF_SERVER_TRANSPORT_CODEC,
28 CONF_STREAM_IDLE_THRESHOLD,
29 CONF_USE_EXTERNAL_SERVER,
30 DEFAULT_SNAPSERVER_IP,
31 DEFAULT_SNAPSERVER_PORT,
32 DEFAULT_SNAPSTREAM_IDLE_THRESHOLD,
33)
34from music_assistant.providers.snapcast.provider import SnapCastProvider
35
36SUPPORTED_FEATURES = {
37 ProviderFeature.SYNC_PLAYERS,
38 ProviderFeature.REMOVE_PLAYER,
39}
40
41
42async def setup(
43 mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
44) -> ProviderInstanceType:
45 """Initialize provider(instance) with given configuration."""
46 return SnapCastProvider(mass, manifest, config, SUPPORTED_FEATURES)
47
48
49async def get_config_entries(
50 mass: MusicAssistant, # noqa: ARG001
51 instance_id: str | None = None, # noqa: ARG001
52 action: str | None = None, # noqa: ARG001
53 values: dict[str, ConfigValueType] | None = None, # noqa: ARG001
54) -> tuple[ConfigEntry, ...]:
55 """
56 Return Config entries to setup this provider.
57
58 :param instance_id: id of an existing provider instance (None if new instance setup).
59 :param action: [optional] action key called from config entries UI.
60 :param values: the (intermediate) raw values for config entries sent with the action.
61 """
62 returncode, output = await check_output("snapserver", "-v")
63 snapserver_version = -1
64 if returncode == 0:
65 # Parse version from output, handling potential noise from library warnings
66 # Expected format: "0.27.0" or similar version string
67 output_str = output.decode()
68 if version_match := re.search(r"(\d+)\.(\d+)\.(\d+)", output_str):
69 snapserver_version = int(version_match.group(2))
70 local_snapserver_present = snapserver_version >= 27 and snapserver_version != 30
71 if returncode == 0 and not local_snapserver_present:
72 raise SetupFailedError(
73 f"Invalid snapserver version. Expected >= 27 and != 30, got {snapserver_version}"
74 )
75
76 return (
77 ConfigEntry(
78 key=CONF_SERVER_BUFFER_SIZE,
79 type=ConfigEntryType.INTEGER,
80 range=(200, 6000),
81 default_value=1000,
82 label="Snapserver buffer size",
83 required=False,
84 category=CONF_CATEGORY_BUILT_IN,
85 hidden=not local_snapserver_present,
86 depends_on=CONF_USE_EXTERNAL_SERVER,
87 depends_on_value_not=True,
88 help_link=CONF_HELP_LINK,
89 ),
90 ConfigEntry(
91 key=CONF_SERVER_CHUNK_MS,
92 type=ConfigEntryType.INTEGER,
93 range=(10, 100),
94 default_value=26,
95 label="Snapserver chunk size",
96 required=False,
97 category=CONF_CATEGORY_BUILT_IN,
98 hidden=not local_snapserver_present,
99 depends_on=CONF_USE_EXTERNAL_SERVER,
100 depends_on_value_not=True,
101 help_link=CONF_HELP_LINK,
102 ),
103 ConfigEntry(
104 key=CONF_SERVER_INITIAL_VOLUME,
105 type=ConfigEntryType.INTEGER,
106 range=(0, 100),
107 default_value=25,
108 label="Snapserver initial volume",
109 required=False,
110 category=CONF_CATEGORY_BUILT_IN,
111 hidden=not local_snapserver_present,
112 depends_on=CONF_USE_EXTERNAL_SERVER,
113 depends_on_value_not=True,
114 help_link=CONF_HELP_LINK,
115 ),
116 ConfigEntry(
117 key=CONF_SERVER_SEND_AUDIO_TO_MUTED,
118 type=ConfigEntryType.BOOLEAN,
119 default_value=False,
120 label="Send audio to muted clients",
121 required=False,
122 category=CONF_CATEGORY_BUILT_IN,
123 hidden=not local_snapserver_present,
124 depends_on=CONF_USE_EXTERNAL_SERVER,
125 depends_on_value_not=True,
126 help_link=CONF_HELP_LINK,
127 ),
128 ConfigEntry(
129 key=CONF_SERVER_TRANSPORT_CODEC,
130 type=ConfigEntryType.STRING,
131 options=[
132 ConfigValueOption(
133 title="FLAC",
134 value="flac",
135 ),
136 ConfigValueOption(
137 title="OGG",
138 value="ogg",
139 ),
140 ConfigValueOption(
141 title="OPUS",
142 value="opus",
143 ),
144 ConfigValueOption(
145 title="PCM",
146 value="pcm",
147 ),
148 ],
149 default_value="flac",
150 label="Snapserver default transport codec",
151 required=False,
152 category=CONF_CATEGORY_BUILT_IN,
153 hidden=not local_snapserver_present,
154 depends_on=CONF_USE_EXTERNAL_SERVER,
155 depends_on_value_not=True,
156 help_link=CONF_HELP_LINK,
157 ),
158 ConfigEntry(
159 key=CONF_USE_EXTERNAL_SERVER,
160 type=ConfigEntryType.BOOLEAN,
161 default_value=not local_snapserver_present,
162 label="Use existing Snapserver",
163 required=False,
164 advanced=local_snapserver_present,
165 ),
166 ConfigEntry(
167 key=CONF_SERVER_HOST,
168 type=ConfigEntryType.STRING,
169 default_value=DEFAULT_SNAPSERVER_IP,
170 label="Snapcast server ip",
171 required=False,
172 depends_on=CONF_USE_EXTERNAL_SERVER,
173 advanced=local_snapserver_present,
174 ),
175 ConfigEntry(
176 key=CONF_SERVER_CONTROL_PORT,
177 type=ConfigEntryType.INTEGER,
178 default_value=DEFAULT_SNAPSERVER_PORT,
179 label="Snapcast control port",
180 required=False,
181 depends_on=CONF_USE_EXTERNAL_SERVER,
182 advanced=local_snapserver_present,
183 ),
184 ConfigEntry(
185 key=CONF_STREAM_IDLE_THRESHOLD,
186 type=ConfigEntryType.INTEGER,
187 default_value=DEFAULT_SNAPSTREAM_IDLE_THRESHOLD,
188 label="Snapcast idle threshold stream parameter",
189 required=True,
190 advanced=local_snapserver_present,
191 ),
192 )
193