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