/
/
/
1"""All constants for Music Assistant."""
2
3import json
4import pathlib
5from copy import deepcopy
6from typing import Any, Final, cast
7
8from music_assistant_models.config_entries import (
9 MULTI_VALUE_SPLITTER,
10 ConfigEntry,
11 ConfigValueOption,
12)
13from music_assistant_models.enums import ConfigEntryType, ContentType, MediaType, PlayerFeature
14from music_assistant_models.media_items import Audiobook, AudioFormat, PodcastEpisode, Radio, Track
15
16APPLICATION_NAME: Final = "Music Assistant"
17
18# Type alias for items that can be added to playlists
19PlaylistPlayableItem = Track | Radio | PodcastEpisode | Audiobook
20
21# Corresponding MediaType enum values (must match PlaylistPlayableItem types above)
22PLAYLIST_MEDIA_TYPES: Final[tuple[MediaType, ...]] = (
23 MediaType.TRACK,
24 MediaType.RADIO,
25 MediaType.PODCAST_EPISODE,
26 MediaType.AUDIOBOOK,
27)
28
29
30API_SCHEMA_VERSION: Final[int] = 29
31MIN_SCHEMA_VERSION: Final[int] = 29
32
33
34MASS_LOGGER_NAME: Final[str] = "music_assistant"
35
36# Home Assistant system user
37HOMEASSISTANT_SYSTEM_USER: Final[str] = "homeassistant_system"
38
39UNKNOWN_ARTIST: Final[str] = "[unknown]"
40UNKNOWN_ARTIST_ID_MBID: Final[str] = "125ec42a-7229-4250-afc5-e057484327fe"
41VARIOUS_ARTISTS_NAME: Final[str] = "Various Artists"
42VARIOUS_ARTISTS_MBID: Final[str] = "89ad4ac3-39f7-470e-963a-56509c546377"
43
44
45RESOURCES_DIR: Final[pathlib.Path] = (
46 pathlib.Path(__file__).parent.resolve().joinpath("helpers/resources")
47)
48GENRE_MAPPING_FILE: Final[pathlib.Path] = RESOURCES_DIR.joinpath("genre_mapping.json")
49
50ANNOUNCE_ALERT_FILE: Final[str] = str(RESOURCES_DIR.joinpath("announce.mp3"))
51SILENCE_FILE: Final[str] = str(RESOURCES_DIR.joinpath("silence.mp3"))
52SILENCE_FILE_LONG: Final[str] = str(RESOURCES_DIR.joinpath("silence_long.ogg"))
53VARIOUS_ARTISTS_FANART: Final[str] = str(RESOURCES_DIR.joinpath("fallback_fanart.jpeg"))
54MASS_LOGO: Final[str] = str(RESOURCES_DIR.joinpath("logo.png"))
55
56
57# config keys
58CONF_ONBOARD_DONE: Final[str] = "onboard_done"
59CONF_SERVER_ID: Final[str] = "server_id"
60CONF_IP_ADDRESS: Final[str] = "ip_address"
61CONF_PORT: Final[str] = "port"
62CONF_PROVIDERS: Final[str] = "providers"
63CONF_PLAYERS: Final[str] = "players"
64CONF_CORE: Final[str] = "core"
65CONF_PATH: Final[str] = "path"
66CONF_NAME: Final[str] = "name"
67CONF_USERNAME: Final[str] = "username"
68CONF_PASSWORD: Final[str] = "password"
69CONF_VOLUME_NORMALIZATION: Final[str] = "volume_normalization"
70CONF_VOLUME_NORMALIZATION_TARGET: Final[str] = "volume_normalization_target"
71CONF_OUTPUT_LIMITER: Final[str] = "output_limiter"
72CONF_PLAYER_DSP: Final[str] = "player_dsp"
73CONF_PLAYER_DSP_PRESETS: Final[str] = "player_dsp_presets"
74CONF_OUTPUT_CHANNELS: Final[str] = "output_channels"
75CONF_FLOW_MODE: Final[str] = "flow_mode"
76CONF_LOG_LEVEL: Final[str] = "log_level"
77CONF_HIDE_GROUP_CHILDS: Final[str] = "hide_group_childs"
78CONF_CROSSFADE_DURATION: Final[str] = "crossfade_duration"
79CONF_BIND_IP: Final[str] = "bind_ip"
80CONF_BIND_PORT: Final[str] = "bind_port"
81CONF_PUBLISH_IP: Final[str] = "publish_ip"
82CONF_AUTO_PLAY: Final[str] = "auto_play"
83CONF_GROUP_MEMBERS: Final[str] = "group_members"
84CONF_DYNAMIC_GROUP_MEMBERS: Final[str] = "dynamic_members"
85CONF_HIDE_IN_UI: Final[str] = "hide_in_ui"
86CONF_EXPOSE_PLAYER_TO_HA: Final[str] = "expose_player_to_ha"
87CONF_SYNC_ADJUST: Final[str] = "sync_adjust"
88CONF_TTS_PRE_ANNOUNCE: Final[str] = "tts_pre_announce"
89CONF_ANNOUNCE_VOLUME_STRATEGY: Final[str] = "announce_volume_strategy"
90CONF_ANNOUNCE_VOLUME: Final[str] = "announce_volume"
91CONF_ANNOUNCE_VOLUME_MIN: Final[str] = "announce_volume_min"
92CONF_ANNOUNCE_VOLUME_MAX: Final[str] = "announce_volume_max"
93CONF_PRE_ANNOUNCE_CHIME_URL: Final[str] = "pre_announcement_chime_url"
94CONF_ICON: Final[str] = "icon"
95CONF_LANGUAGE: Final[str] = "language"
96CONF_SAMPLE_RATES: Final[str] = "sample_rates"
97CONF_HTTP_PROFILE: Final[str] = "http_profile"
98CONF_BYPASS_NORMALIZATION_RADIO: Final[str] = "bypass_normalization_radio"
99CONF_ENABLE_ICY_METADATA: Final[str] = "enable_icy_metadata"
100CONF_VOLUME_NORMALIZATION_RADIO: Final[str] = "volume_normalization_radio"
101CONF_VOLUME_NORMALIZATION_TRACKS: Final[str] = "volume_normalization_tracks"
102CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO: Final[str] = "volume_normalization_fixed_gain_radio"
103CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS: Final[str] = "volume_normalization_fixed_gain_tracks"
104CONF_POWER_CONTROL: Final[str] = "power_control"
105CONF_VOLUME_CONTROL: Final[str] = "volume_control"
106CONF_MUTE_CONTROL: Final[str] = "mute_control"
107CONF_PREFERRED_OUTPUT_PROTOCOL: Final[str] = "preferred_output_protocol"
108CONF_LINKED_PROTOCOL_PLAYER_IDS: Final[str] = (
109 "linked_protocol_player_ids" # cached for fast restart
110)
111CONF_PROTOCOL_PARENT_ID: Final[str] = (
112 "protocol_parent_id" # cached native player ID for protocol player
113)
114CONF_OUTPUT_CODEC: Final[str] = "output_codec"
115CONF_ALLOW_AUDIO_CACHE: Final[str] = "allow_audio_cache"
116CONF_SMART_FADES_MODE: Final[str] = "smart_fades_mode"
117CONF_USE_SSL: Final[str] = "use_ssl"
118CONF_VERIFY_SSL: Final[str] = "verify_ssl"
119CONF_SSL_FINGERPRINT: Final[str] = "ssl_fingerprint"
120CONF_AUTH_ALLOW_SELF_REGISTRATION: Final[str] = "auth_allow_self_registration"
121CONF_ZEROCONF_INTERFACES: Final[str] = "zeroconf_interfaces"
122CONF_ENABLED: Final[str] = "enabled"
123CONF_PROTOCOL_KEY_SPLITTER: Final[str] = "||protocol||"
124CONF_PROTOCOL_CATEGORY_PREFIX: Final[str] = "protocol"
125CONF_DEFAULT_PROVIDERS_SETUP: Final[str] = "default_providers_setup"
126
127
128# config default values
129DEFAULT_HOST: Final[str] = "0.0.0.0"
130DEFAULT_PORT: Final[int] = 8095
131
132
133# common db tables
134DB_TABLE_PLAYLOG: Final[str] = "playlog"
135DB_TABLE_ARTISTS: Final[str] = "artists"
136DB_TABLE_ALBUMS: Final[str] = "albums"
137DB_TABLE_TRACKS: Final[str] = "tracks"
138DB_TABLE_PLAYLISTS: Final[str] = "playlists"
139DB_TABLE_RADIOS: Final[str] = "radios"
140DB_TABLE_AUDIOBOOKS: Final[str] = "audiobooks"
141DB_TABLE_PODCASTS: Final[str] = "podcasts"
142DB_TABLE_CACHE: Final[str] = "cache"
143DB_TABLE_SETTINGS: Final[str] = "settings"
144DB_TABLE_THUMBS: Final[str] = "thumbnails"
145DB_TABLE_PROVIDER_MAPPINGS: Final[str] = "provider_mappings"
146DB_TABLE_ALBUM_TRACKS: Final[str] = "album_tracks"
147DB_TABLE_TRACK_ARTISTS: Final[str] = "track_artists"
148DB_TABLE_ALBUM_ARTISTS: Final[str] = "album_artists"
149DB_TABLE_LOUDNESS_MEASUREMENTS: Final[str] = "loudness_measurements"
150DB_TABLE_SMART_FADES_ANALYSIS: Final[str] = "smart_fades_analysis"
151DB_TABLE_GENRES: Final[str] = "genres"
152DB_TABLE_GENRE_MEDIA_ITEM_MAPPING: Final[str] = "genre_media_item_mapping"
153
154
155def load_genre_mapping() -> list[dict[str, Any]]:
156 """Load default genre mapping from JSON file.
157
158 :return: List of genre mapping dictionaries with 'genre' and 'aliases' keys.
159 :raises FileNotFoundError: If genre_mapping.json is missing.
160 :raises ValueError: If JSON is malformed or missing required fields.
161 """
162 try:
163 content = GENRE_MAPPING_FILE.read_text(encoding="utf-8")
164 data = json.loads(content)
165 except FileNotFoundError as err:
166 msg = f"Genre mapping file not found: {GENRE_MAPPING_FILE}"
167 raise FileNotFoundError(msg) from err
168 except json.JSONDecodeError as err:
169 msg = f"Invalid JSON in genre mapping file: {GENRE_MAPPING_FILE}"
170 raise ValueError(msg) from err
171
172 if not isinstance(data, list):
173 msg = f"Genre mapping must be a list, got {type(data).__name__}"
174 raise TypeError(msg)
175
176 for idx, entry in enumerate(data):
177 if not isinstance(entry, dict):
178 msg = f"Genre mapping entry {idx} must be a dict, got {type(entry).__name__}"
179 raise TypeError(msg)
180 if "genre" not in entry:
181 msg = f"Genre mapping entry {idx} missing required field 'genre'"
182 raise ValueError(msg)
183 if "aliases" not in entry:
184 msg = f"Genre mapping entry {idx} missing required field 'aliases'"
185 raise ValueError(msg)
186
187 return cast("list[dict[str, Any]]", data)
188
189
190DEFAULT_GENRE_MAPPING: Final[list[dict[str, Any]]] = load_genre_mapping()
191DEFAULT_GENRES: Final[tuple[str, ...]] = tuple(entry["genre"] for entry in DEFAULT_GENRE_MAPPING)
192
193
194# all other
195MASS_LOGO_ONLINE: Final[str] = (
196 "https://github.com/music-assistant/server/blob/dev/music_assistant/logo.png"
197)
198ENCRYPT_SUFFIX = "_encrypted_"
199CONFIGURABLE_CORE_CONTROLLERS = (
200 "streams",
201 "webserver",
202 "players",
203 "metadata",
204 "cache",
205 "music",
206 "player_queues",
207)
208VERBOSE_LOG_LEVEL: Final[int] = 5
209PROVIDERS_WITH_SHAREABLE_URLS = ("spotify", "qobuz")
210
211
212####### REUSABLE CONFIG ENTRIES #######
213
214CONF_ENTRY_LOG_LEVEL = ConfigEntry(
215 key=CONF_LOG_LEVEL,
216 type=ConfigEntryType.STRING,
217 label="Log level",
218 options=[
219 ConfigValueOption("global", "GLOBAL"),
220 ConfigValueOption("info", "INFO"),
221 ConfigValueOption("warning", "WARNING"),
222 ConfigValueOption("error", "ERROR"),
223 ConfigValueOption("debug", "DEBUG"),
224 ConfigValueOption("verbose", "VERBOSE"),
225 ],
226 default_value="GLOBAL",
227 advanced=True,
228 requires_reload=False, # applied dynamically via _set_logger()
229)
230
231DEFAULT_PROVIDER_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,)
232DEFAULT_CORE_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,)
233
234# some reusable player config entries
235
236CONF_ENTRY_FLOW_MODE = ConfigEntry(
237 key=CONF_FLOW_MODE,
238 type=ConfigEntryType.BOOLEAN,
239 label="Enforce Gapless playback with Queue Flow Mode streaming",
240 default_value=False,
241 category="protocol_generic",
242 advanced=True,
243 requires_reload=True,
244)
245
246
247CONF_ENTRY_AUTO_PLAY = ConfigEntry(
248 key=CONF_AUTO_PLAY,
249 type=ConfigEntryType.BOOLEAN,
250 label="Automatically play/resume on power on",
251 default_value=False,
252 description="When this player is turned ON, automatically start playing "
253 "(if there are items in the queue).",
254 depends_on=CONF_POWER_CONTROL,
255 depends_on_value_not="none",
256 category="player_controls",
257)
258
259CONF_ENTRY_OUTPUT_CHANNELS = ConfigEntry(
260 key=CONF_OUTPUT_CHANNELS,
261 type=ConfigEntryType.STRING,
262 options=[
263 ConfigValueOption("Stereo (both channels)", "stereo"),
264 ConfigValueOption("Left channel", "left"),
265 ConfigValueOption("Right channel", "right"),
266 ConfigValueOption("Mono (both channels)", "mono"),
267 ],
268 default_value="stereo",
269 label="Output Channel Mode",
270 category="protocol_generic",
271 advanced=True,
272 requires_reload=True,
273)
274
275CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry(
276 key=CONF_VOLUME_NORMALIZATION,
277 type=ConfigEntryType.BOOLEAN,
278 label="Enable volume normalization",
279 default_value=True,
280 description="Enable volume normalization (EBU-R128 based)",
281 category="playback",
282 requires_reload=True,
283)
284
285CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry(
286 key=CONF_VOLUME_NORMALIZATION_TARGET,
287 type=ConfigEntryType.INTEGER,
288 range=(-30, -5),
289 default_value=-17,
290 label="Target level for volume normalization",
291 description="Adjust average (perceived) loudness to this target level",
292 depends_on=CONF_VOLUME_NORMALIZATION,
293 category="playback",
294 advanced=True,
295 requires_reload=True,
296)
297
298CONF_ENTRY_OUTPUT_LIMITER = ConfigEntry(
299 key=CONF_OUTPUT_LIMITER,
300 type=ConfigEntryType.BOOLEAN,
301 label="Enable limiting to prevent clipping",
302 default_value=True,
303 description="Activates a limiter that prevents audio distortion by making loud peaks quieter.",
304 category="playback",
305 advanced=True,
306 requires_reload=True,
307)
308
309
310CONF_ENTRY_SMART_FADES_MODE = ConfigEntry(
311 key=CONF_SMART_FADES_MODE,
312 type=ConfigEntryType.STRING,
313 label="Enable Smart Fades",
314 options=[
315 ConfigValueOption("Disabled", "disabled"),
316 ConfigValueOption("Smart Crossfade", "smart_crossfade"),
317 ConfigValueOption("Standard Crossfade", "standard_crossfade"),
318 ],
319 default_value="disabled",
320 description="Select the crossfade mode to use when transitioning between tracks.\n\n"
321 "- 'Smart Crossfade': Uses beat matching and EQ filters to create smooth transitions"
322 " between tracks.\n"
323 "- 'Standard Crossfade': Regular crossfade that crossfades the last/first x-seconds of a "
324 "track.",
325 category="playback",
326 requires_reload=True,
327)
328
329CONF_ENTRY_CROSSFADE_DURATION = ConfigEntry(
330 key=CONF_CROSSFADE_DURATION,
331 type=ConfigEntryType.INTEGER,
332 range=(1, 15),
333 default_value=8,
334 label="Fallback crossfade duration",
335 description="Duration in seconds of the standard crossfade between tracks when"
336 " 'Enable Smart Fade' has been set to 'Standard Crossfade' or when a Smart Fade fails",
337 depends_on=CONF_SMART_FADES_MODE,
338 depends_on_value="standard_crossfade",
339 category="playback",
340 advanced=True,
341 requires_reload=True,
342)
343
344
345CONF_ENTRY_OUTPUT_CODEC = ConfigEntry(
346 key=CONF_OUTPUT_CODEC,
347 type=ConfigEntryType.STRING,
348 label="Output codec to use for streaming audio to the player",
349 default_value="flac",
350 options=[
351 ConfigValueOption("FLAC (lossless, compressed)", "flac"),
352 ConfigValueOption("MP3 (lossy)", "mp3"),
353 ConfigValueOption("AAC (lossy)", "aac"),
354 ConfigValueOption("WAV (lossless, uncompressed)", "wav"),
355 ],
356 description="Select the codec to use for streaming audio to this player. \n"
357 "By default, Music Assistant sends lossless, high quality audio to all players and prefers "
358 "the FLAC codec because it offers some compression while still remaining lossless \n\n"
359 "Some players however do not support FLAC and require the stream to be packed "
360 "into e.g. a lossy mp3 codec or you like to save some network bandwidth. \n\n "
361 "Choosing a lossy codec saves some bandwidth at the cost of audio quality.",
362 category="protocol_generic",
363 advanced=True,
364 requires_reload=True,
365)
366
367CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3 = ConfigEntry.from_dict(
368 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "mp3"}
369)
370CONF_ENTRY_OUTPUT_CODEC_ENFORCE_MP3 = ConfigEntry.from_dict(
371 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "mp3", "hidden": True}
372)
373CONF_ENTRY_OUTPUT_CODEC_HIDDEN = ConfigEntry.from_dict(
374 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "hidden": True}
375)
376CONF_ENTRY_OUTPUT_CODEC_ENFORCE_FLAC = ConfigEntry.from_dict(
377 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "flac", "hidden": True}
378)
379
380
381def create_output_codec_config_entry(
382 hidden: bool = False, default_value: str = "flac"
383) -> ConfigEntry:
384 """Create output codec config entry based on player specific helpers."""
385 conf_entry = ConfigEntry.from_dict(CONF_ENTRY_OUTPUT_CODEC.to_dict())
386 conf_entry.hidden = hidden
387 conf_entry.default_value = default_value
388 return conf_entry
389
390
391CONF_ENTRY_SYNC_ADJUST = ConfigEntry(
392 key=CONF_SYNC_ADJUST,
393 type=ConfigEntryType.INTEGER,
394 range=(-500, 500),
395 default_value=0,
396 label="Audio synchronization delay correction",
397 description="If this player is playing audio synced with other players "
398 "and you always hear the audio too early or late on this player, "
399 "you can shift the audio a bit.",
400 category="protocol_generic",
401 advanced=True,
402 requires_reload=True,
403)
404
405
406CONF_ENTRY_TTS_PRE_ANNOUNCE = ConfigEntry(
407 key=CONF_TTS_PRE_ANNOUNCE,
408 type=ConfigEntryType.BOOLEAN,
409 default_value=True,
410 label="Pre-announce TTS announcements",
411 category="announcements",
412)
413
414
415CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY = ConfigEntry(
416 key=CONF_ANNOUNCE_VOLUME_STRATEGY,
417 type=ConfigEntryType.STRING,
418 options=[
419 ConfigValueOption("Absolute volume", "absolute"),
420 ConfigValueOption("Relative volume increase", "relative"),
421 ConfigValueOption("Volume increase by fixed percentage", "percentual"),
422 ConfigValueOption("Do not adjust volume", "none"),
423 ],
424 default_value="percentual",
425 label="Volume strategy for Announcements",
426 category="announcements",
427)
428
429CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY_HIDDEN = ConfigEntry.from_dict(
430 {**CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.to_dict(), "hidden": True}
431)
432
433CONF_ENTRY_ANNOUNCE_VOLUME = ConfigEntry(
434 key=CONF_ANNOUNCE_VOLUME,
435 type=ConfigEntryType.INTEGER,
436 default_value=85,
437 label="Volume for Announcements",
438 category="announcements",
439)
440CONF_ENTRY_ANNOUNCE_VOLUME_HIDDEN = ConfigEntry.from_dict(
441 {**CONF_ENTRY_ANNOUNCE_VOLUME.to_dict(), "hidden": True}
442)
443
444CONF_ENTRY_ANNOUNCE_VOLUME_MIN = ConfigEntry(
445 key=CONF_ANNOUNCE_VOLUME_MIN,
446 type=ConfigEntryType.INTEGER,
447 default_value=15,
448 label="Minimum Volume level for Announcements",
449 description="The volume (adjustment) of announcements should no go below this level.",
450 category="announcements",
451)
452CONF_ENTRY_ANNOUNCE_VOLUME_MIN_HIDDEN = ConfigEntry.from_dict(
453 {**CONF_ENTRY_ANNOUNCE_VOLUME_MIN.to_dict(), "hidden": True}
454)
455
456CONF_ENTRY_ANNOUNCE_VOLUME_MAX = ConfigEntry(
457 key=CONF_ANNOUNCE_VOLUME_MAX,
458 type=ConfigEntryType.INTEGER,
459 default_value=75,
460 label="Maximum Volume level for Announcements",
461 description="The volume (adjustment) of announcements should no go above this level.",
462 category="announcements",
463)
464CONF_ENTRY_ANNOUNCE_VOLUME_MAX_HIDDEN = ConfigEntry.from_dict(
465 {**CONF_ENTRY_ANNOUNCE_VOLUME_MAX.to_dict(), "hidden": True}
466)
467
468
469HIDDEN_ANNOUNCE_VOLUME_CONFIG_ENTRIES = (
470 CONF_ENTRY_ANNOUNCE_VOLUME_HIDDEN,
471 CONF_ENTRY_ANNOUNCE_VOLUME_MIN_HIDDEN,
472 CONF_ENTRY_ANNOUNCE_VOLUME_MAX_HIDDEN,
473 CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY_HIDDEN,
474)
475
476
477CONF_ENTRY_SAMPLE_RATES = ConfigEntry(
478 key=CONF_SAMPLE_RATES,
479 type=ConfigEntryType.SPLITTED_STRING,
480 multi_value=True,
481 options=[
482 ConfigValueOption("44.1kHz / 16 bits", f"44100{MULTI_VALUE_SPLITTER}16"),
483 ConfigValueOption("44.1kHz / 24 bits", f"44100{MULTI_VALUE_SPLITTER}24"),
484 ConfigValueOption("48kHz / 16 bits", f"48000{MULTI_VALUE_SPLITTER}16"),
485 ConfigValueOption("48kHz / 24 bits", f"48000{MULTI_VALUE_SPLITTER}24"),
486 ConfigValueOption("88.2kHz / 16 bits", f"88200{MULTI_VALUE_SPLITTER}16"),
487 ConfigValueOption("88.2kHz / 24 bits", f"88200{MULTI_VALUE_SPLITTER}24"),
488 ConfigValueOption("96kHz / 16 bits", f"96000{MULTI_VALUE_SPLITTER}16"),
489 ConfigValueOption("96kHz / 24 bits", f"96000{MULTI_VALUE_SPLITTER}24"),
490 ConfigValueOption("176.4kHz / 16 bits", f"176400{MULTI_VALUE_SPLITTER}16"),
491 ConfigValueOption("176.4kHz / 24 bits", f"176400{MULTI_VALUE_SPLITTER}24"),
492 ConfigValueOption("192kHz / 16 bits", f"192000{MULTI_VALUE_SPLITTER}16"),
493 ConfigValueOption("192kHz / 24 bits", f"192000{MULTI_VALUE_SPLITTER}24"),
494 ConfigValueOption("352.8kHz / 16 bits", f"352800{MULTI_VALUE_SPLITTER}16"),
495 ConfigValueOption("352.8kHz / 24 bits", f"352800{MULTI_VALUE_SPLITTER}24"),
496 ConfigValueOption("384kHz / 16 bits", f"384000{MULTI_VALUE_SPLITTER}16"),
497 ConfigValueOption("384kHz / 24 bits", f"384000{MULTI_VALUE_SPLITTER}24"),
498 ],
499 default_value=[f"44100{MULTI_VALUE_SPLITTER}16", f"48000{MULTI_VALUE_SPLITTER}16"],
500 required=True,
501 label="Sample rates supported by this player",
502 category="protocol_generic",
503 advanced=True,
504 description="The sample rates (and bit depths) supported by this player.\n"
505 "Content with unsupported sample rates will be automatically resampled.",
506 requires_reload=True,
507)
508
509
510CONF_ENTRY_HTTP_PROFILE = ConfigEntry(
511 key=CONF_HTTP_PROFILE,
512 type=ConfigEntryType.STRING,
513 options=[
514 ConfigValueOption("Profile 1 - chunked", "chunked"),
515 ConfigValueOption("Profile 2 - no content length", "no_content_length"),
516 ConfigValueOption("Profile 3 - forced content length", "forced_content_length"),
517 ],
518 default_value="no_content_length",
519 label="HTTP Profile used for sending audio",
520 category="protocol_generic",
521 advanced=True,
522 description="This is considered to be a very advanced setting, only adjust this if needed, "
523 "for example if your player stops playing halfway streams or if you experience "
524 "other playback related issues. In most cases the default setting is fine.",
525 requires_reload=True,
526)
527
528CONF_ENTRY_HTTP_PROFILE_DEFAULT_1 = ConfigEntry.from_dict(
529 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "chunked"}
530)
531
532CONF_ENTRY_HTTP_PROFILE_DEFAULT_2 = ConfigEntry.from_dict(
533 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "no_content_length"}
534)
535CONF_ENTRY_HTTP_PROFILE_DEFAULT_3 = ConfigEntry.from_dict(
536 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "forced_content_length"}
537)
538
539CONF_ENTRY_HTTP_PROFILE_FORCED_1 = ConfigEntry.from_dict(
540 {**CONF_ENTRY_HTTP_PROFILE_DEFAULT_1.to_dict(), "hidden": True}
541)
542CONF_ENTRY_HTTP_PROFILE_FORCED_2 = ConfigEntry.from_dict(
543 {
544 **CONF_ENTRY_HTTP_PROFILE.to_dict(),
545 "default_value": "no_content_length",
546 "hidden": True,
547 }
548)
549CONF_ENTRY_HTTP_PROFILE_HIDDEN = ConfigEntry.from_dict(
550 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "hidden": True}
551)
552
553
554CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry(
555 key=CONF_ENABLE_ICY_METADATA,
556 type=ConfigEntryType.STRING,
557 options=[
558 ConfigValueOption("Disabled - do not send ICY metadata", "disabled"),
559 ConfigValueOption("Profile 1 - basic info", "basic"),
560 ConfigValueOption("Profile 2 - full info (including image)", "full"),
561 ],
562 depends_on=CONF_FLOW_MODE,
563 depends_on_value_not=False,
564 default_value="disabled",
565 label="Try to inject metadata into stream (ICY)",
566 category="protocol_generic",
567 advanced=True,
568 description="Try to inject metadata into the stream (ICY) to show track info on the player, "
569 "even when flow mode is enabled.\n\nThis is called ICY metadata and is what is used by "
570 "online radio stations to show you what is playing. \n\nBe aware that not all players support "
571 "this correctly. If you experience issues with playback, try disabling this setting.",
572 requires_reload=True,
573)
574
575CONF_ENTRY_ENABLE_ICY_METADATA_HIDDEN = ConfigEntry.from_dict(
576 {**CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(), "hidden": True}
577)
578
579CONF_ENTRY_ICY_METADATA_HIDDEN_DISABLED = ConfigEntry.from_dict(
580 {
581 **CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(),
582 "default_value": "disabled",
583 "value": "disabled",
584 "hidden": True,
585 }
586)
587
588CONF_ENTRY_ICY_METADATA_DEFAULT_FULL = ConfigEntry.from_dict(
589 {
590 **CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(),
591 "default_value": "full",
592 }
593)
594
595CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES = ConfigEntry(
596 key="gapless_different_sample_rates",
597 type=ConfigEntryType.BOOLEAN,
598 label="Allow gapless playback (and crossfades) between tracks of different sample rates",
599 description="Enable this option to allow gapless playback between tracks that have different "
600 "sample rates (e.g. 44.1kHz to 48kHz). \n\n "
601 "Only enable this option if your player actually support this, otherwise you may "
602 "experience audio glitches during transitioning between tracks.",
603 default_value=False,
604 category="protocol_generic",
605 advanced=True,
606 requires_reload=True,
607)
608
609CONF_ENTRY_WARN_PREVIEW = ConfigEntry(
610 key="preview_note",
611 type=ConfigEntryType.ALERT,
612 label="Please note that this feature/provider is still in early stages. \n\n"
613 "Functionality may still be limited and/or bugs may occur!",
614 required=False,
615)
616
617CONF_ENTRY_MANUAL_DISCOVERY_IPS = ConfigEntry(
618 key="manual_discovery_ip_addresses",
619 type=ConfigEntryType.STRING,
620 label="Manual IP addresses for discovery",
621 description="In normal circumstances, "
622 "Music Assistant will automatically discover all players on the network. "
623 "using multicast discovery on the (L2) local network, such as mDNS or UPNP.\n\n"
624 "In case of special network setups or when you run into issues where "
625 "one or more players are not discovered, you can manually add the IP "
626 "addresses of the players here. \n\n"
627 "Note that this setting is not recommended for normal use and should only be used "
628 "if you know what you are doing. Also, if players are not on the same subnet as"
629 "the Music Assistant server, you may run into issues with streaming. "
630 "In that case always ensure that the players can reach the server on the network "
631 "and double check the base URL configuration of the Stream server in the settings.",
632 advanced=True,
633 default_value=[],
634 required=False,
635 multi_value=True,
636)
637
638CONF_ENTRY_LIBRARY_SYNC_ARTISTS = ConfigEntry(
639 key="library_sync_artists",
640 type=ConfigEntryType.BOOLEAN,
641 label="Sync Library Artists from this provider to Music Assistant",
642 description="Whether to synchronize (favourited/in-library) Artists from this "
643 "provider to the Music Assistant Library.",
644 default_value=True,
645 category="sync_options",
646)
647
648
649CONF_ENTRY_ZEROCONF_INTERFACES = ConfigEntry(
650 key=CONF_ZEROCONF_INTERFACES,
651 type=ConfigEntryType.STRING,
652 label="Mdns/Zeroconf discovery interface(s)",
653 description="In normal circumstances, Music Assistant will automatically "
654 "discover all players on the network using multicast discovery on the "
655 "(L2) local network, such as mDNS or UPNP.\n\n"
656 "By default, Music Assistant will only listen on the default interface. "
657 "If you have multiple network interfaces and you want to discover players "
658 "on all interfaces, you can change this setting to 'All interfaces'.",
659 options=[
660 ConfigValueOption("Default interface", "default"),
661 ConfigValueOption("All interfaces", "all"),
662 ],
663 default_value="default",
664 advanced=True,
665 requires_reload=True,
666)
667CONF_ENTRY_LIBRARY_SYNC_ALBUMS = ConfigEntry(
668 key="library_sync_albums",
669 type=ConfigEntryType.BOOLEAN,
670 label="Sync Library Albums from this provider to Music Assistant",
671 description="Whether to import (favourited/in-library) Albums from this "
672 "provider to the Music Assistant Library. \n\n"
673 "Please note that by adding an Album into the Music Assistant library, "
674 "the Album Artists will always be imported as well.",
675 default_value=True,
676 category="sync_options",
677)
678CONF_ENTRY_LIBRARY_SYNC_TRACKS = ConfigEntry(
679 key="library_sync_tracks",
680 type=ConfigEntryType.BOOLEAN,
681 label="Sync Library Tracks from this provider to Music Assistant",
682 description="Whether to import (favourited/in-library) Tracks from this "
683 "provider to the Music Assistant Library. \n\n"
684 "Please note that by adding a Track into the Music Assistant library, "
685 "the Track's Artists and Album will always be imported as well.",
686 default_value=True,
687 category="sync_options",
688)
689CONF_ENTRY_LIBRARY_SYNC_PLAYLISTS = ConfigEntry(
690 key="library_sync_playlists",
691 type=ConfigEntryType.BOOLEAN,
692 label="Sync Library Playlists from this provider to Music Assistant",
693 description="Whether to import (favourited/in-library) Playlists from this "
694 "provider to the Music Assistant Library.",
695 default_value=True,
696 category="sync_options",
697)
698CONF_ENTRY_LIBRARY_SYNC_PODCASTS = ConfigEntry(
699 key="library_sync_podcasts",
700 type=ConfigEntryType.BOOLEAN,
701 label="Sync Library Podcasts from this provider to Music Assistant",
702 description="Whether to import (favourited/in-library) Podcasts from this "
703 "provider to the Music Assistant Library.",
704 default_value=True,
705 category="sync_options",
706)
707CONF_ENTRY_LIBRARY_SYNC_AUDIOBOOKS = ConfigEntry(
708 key="library_sync_audiobooks",
709 type=ConfigEntryType.BOOLEAN,
710 label="Sync Library Audiobooks from this provider to Music Assistant",
711 description="Whether to import (favourited/in-library) Audiobooks from this "
712 "provider to the Music Assistant Library.",
713 default_value=True,
714 category="sync_options",
715)
716CONF_ENTRY_LIBRARY_SYNC_RADIOS = ConfigEntry(
717 key="library_sync_radios",
718 type=ConfigEntryType.BOOLEAN,
719 label="Sync Library Radios from this provider to Music Assistant",
720 description="Whether to import (favourited/in-library) Radio stations from this "
721 "provider to the Music Assistant Library.",
722 default_value=True,
723 category="sync_options",
724)
725CONF_ENTRY_LIBRARY_SYNC_ALBUM_TRACKS = ConfigEntry(
726 key="library_sync_album_tracks",
727 type=ConfigEntryType.BOOLEAN,
728 label="Import album tracks",
729 description="By default, when importing Albums into the library, "
730 "only the Album itself will be imported into the Music Assistant Library, "
731 "allowing you to manually browse and select which tracks you want to import. \n\n"
732 "If you want to override this default behavior, "
733 "you can use this configuration option.\n\n"
734 "Please note that some (streaming) providers may already define this behavior unsolicited, "
735 "by automatically adding all tracks from the album to their library/favorites.",
736 default_value=False,
737 category="sync_options",
738)
739CONF_ENTRY_LIBRARY_SYNC_PLAYLIST_TRACKS = ConfigEntry(
740 key="library_sync_playlist_tracks",
741 type=ConfigEntryType.STRING,
742 label="Import playlist tracks",
743 description="By default, when importing Playlists into the library, "
744 "only the Playlist itself will be imported into the Music Assistant Library, "
745 "allowing you to browse and play the Playlist and optionally add any individual "
746 "tracks of the Playlist to the Music Assistant Library manually. \n\n"
747 "Use this configuration option to override this default behavior, "
748 "by specifying the Playlists for which you'd like to import all tracks.\n"
749 "You can either enter the Playlist name (case sensitive) or the Playlist URI.",
750 default_value=[],
751 category="sync_options",
752 multi_value=True,
753)
754
755CONF_ENTRY_LIBRARY_SYNC_BACK = ConfigEntry(
756 key="library_sync_back",
757 type=ConfigEntryType.BOOLEAN,
758 label="Sync back library additions/removals (2-way sync)",
759 description="Specify the behavior if an item is manually added to "
760 "(or removed from) the Music Assistant Library. \n"
761 "Should we synchronise that action back to the provider?\n\n"
762 "Please note that if you you don't sync back to the provider and you have enabled "
763 "automatic sync/import for this provider, a removed item may reappear in the library "
764 "the next time a sync is performed.",
765 default_value=True,
766 category="sync_options",
767)
768
769CONF_ENTRY_LIBRARY_SYNC_DELETIONS = ConfigEntry(
770 key="library_sync_deletions",
771 type=ConfigEntryType.BOOLEAN,
772 label="Sync library deletions",
773 description="When enabled, items removed from the provider's library will also be "
774 "hidden from the Music Assistant library.\n\n"
775 "When disabled, items removed from the provider will remain visible in the "
776 "Music Assistant library.",
777 default_value=True,
778 category="sync_options",
779 advanced=True,
780)
781
782
783CONF_PROVIDER_SYNC_INTERVAL_OPTIONS = [
784 ConfigValueOption("Disable automatic sync for this mediatype", 0),
785 ConfigValueOption("Every 30 minutes", 30),
786 ConfigValueOption("Every hour", 60),
787 ConfigValueOption("Every 3 hours", 180),
788 ConfigValueOption("Every 6 hours", 360),
789 ConfigValueOption("Every 12 hours", 720),
790 ConfigValueOption("Every 24 hours", 1440),
791 ConfigValueOption("Every 36 hours", 2160),
792 ConfigValueOption("Every 48 hours", 2880),
793 ConfigValueOption("Once a week", 10080),
794]
795CONF_ENTRY_PROVIDER_SYNC_INTERVAL_ARTISTS = ConfigEntry(
796 key="provider_sync_interval_artists",
797 type=ConfigEntryType.INTEGER,
798 label="Automatic Sync Interval for Artists",
799 description="The interval at which the Artists are synced to the library for this provider.",
800 options=CONF_PROVIDER_SYNC_INTERVAL_OPTIONS,
801 default_value=720,
802 category="sync_options",
803 depends_on=CONF_ENTRY_LIBRARY_SYNC_ARTISTS.key,
804 depends_on_value=True,
805 required=True,
806)
807CONF_ENTRY_PROVIDER_SYNC_INTERVAL_ALBUMS = ConfigEntry(
808 key="provider_sync_interval_albums",
809 type=ConfigEntryType.INTEGER,
810 label="Automatic Sync Interval for Albums",
811 description="The interval at which the Albums are synced to the library for this provider.",
812 options=CONF_PROVIDER_SYNC_INTERVAL_OPTIONS,
813 default_value=720,
814 category="sync_options",
815 depends_on=CONF_ENTRY_LIBRARY_SYNC_ALBUMS.key,
816 depends_on_value=True,
817 required=True,
818)
819CONF_ENTRY_PROVIDER_SYNC_INTERVAL_TRACKS = ConfigEntry(
820 key="provider_sync_interval_tracks",
821 type=ConfigEntryType.INTEGER,
822 label="Automatic Sync Interval for Tracks",
823 description="The interval at which the Tracks are synced to the library for this provider.",
824 options=CONF_PROVIDER_SYNC_INTERVAL_OPTIONS,
825 default_value=720,
826 category="sync_options",
827 depends_on=CONF_ENTRY_LIBRARY_SYNC_TRACKS.key,
828 depends_on_value=True,
829 required=True,
830)
831CONF_ENTRY_PROVIDER_SYNC_INTERVAL_PLAYLISTS = ConfigEntry(
832 key="provider_sync_interval_playlists",
833 type=ConfigEntryType.INTEGER,
834 label="Automatic Sync Interval for Playlists",
835 description="The interval at which the Playlists are synced to the library for this provider.",
836 options=CONF_PROVIDER_SYNC_INTERVAL_OPTIONS,
837 default_value=720,
838 category="sync_options",
839 depends_on=CONF_ENTRY_LIBRARY_SYNC_PLAYLISTS.key,
840 depends_on_value=True,
841 required=True,
842)
843CONF_ENTRY_PROVIDER_SYNC_INTERVAL_PODCASTS = ConfigEntry(
844 key="provider_sync_interval_podcasts",
845 type=ConfigEntryType.INTEGER,
846 label="Automatic Sync Interval for Podcasts",
847 description="The interval at which the Podcasts are synced to the library for this provider.",
848 options=CONF_PROVIDER_SYNC_INTERVAL_OPTIONS,
849 default_value=720,
850 category="sync_options",
851 depends_on=CONF_ENTRY_LIBRARY_SYNC_PODCASTS.key,
852 depends_on_value=True,
853 required=True,
854)
855CONF_ENTRY_PROVIDER_SYNC_INTERVAL_AUDIOBOOKS = ConfigEntry(
856 key="provider_sync_interval_audiobooks",
857 type=ConfigEntryType.INTEGER,
858 label="Automatic Sync Interval for Audiobooks",
859 description="The interval at which the Audiobooks are synced to the library for this provider.",
860 options=CONF_PROVIDER_SYNC_INTERVAL_OPTIONS,
861 default_value=720,
862 category="sync_options",
863 depends_on=CONF_ENTRY_LIBRARY_SYNC_AUDIOBOOKS.key,
864 depends_on_value=True,
865 required=True,
866)
867CONF_ENTRY_PROVIDER_SYNC_INTERVAL_RADIOS = ConfigEntry(
868 key="provider_sync_interval_radios",
869 type=ConfigEntryType.INTEGER,
870 label="Automatic Sync Interval for Radios",
871 description="The interval at which the Radios are synced to the library for this provider.",
872 options=CONF_PROVIDER_SYNC_INTERVAL_OPTIONS,
873 default_value=720,
874 category="sync_options",
875 depends_on=CONF_ENTRY_LIBRARY_SYNC_RADIOS.key,
876 depends_on_value=True,
877 required=True,
878)
879
880
881CONF_ENTRY_PLAYER_ICON = ConfigEntry(
882 key=CONF_ICON,
883 type=ConfigEntryType.ICON,
884 default_value="mdi-speaker",
885 label="Icon",
886 description="Material design icon for this player. "
887 "\n\nSee https://pictogrammers.com/library/mdi/",
888 category="generic",
889)
890
891CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict(
892 {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "mdi-speaker-multiple"}
893)
894
895
896def create_sample_rates_config_entry(
897 supported_sample_rates: list[int] | None = None,
898 supported_bit_depths: list[int] | None = None,
899 hidden: bool = False,
900 max_sample_rate: int | None = None,
901 max_bit_depth: int | None = None,
902 safe_max_sample_rate: int = 48000,
903 safe_max_bit_depth: int = 16,
904) -> ConfigEntry:
905 """Create sample rates config entry based on player specific helpers."""
906 assert CONF_ENTRY_SAMPLE_RATES.options
907 # if no supported sample rates are defined, we apply the default 44100 as only option
908 if not supported_sample_rates and max_sample_rate is None:
909 supported_sample_rates = [44100]
910 if not supported_bit_depths and max_bit_depth is None:
911 supported_bit_depths = [16]
912 final_supported_sample_rates = supported_sample_rates or []
913 final_supported_bit_depths = supported_bit_depths or []
914 conf_entry = deepcopy(CONF_ENTRY_SAMPLE_RATES)
915 conf_entry.hidden = hidden
916 options: list[ConfigValueOption] = []
917 default_value: list[str] = []
918
919 for option in CONF_ENTRY_SAMPLE_RATES.options:
920 option_value = cast("str", option.value)
921 sample_rate_str, bit_depth_str = option_value.split(MULTI_VALUE_SPLITTER, 1)
922 sample_rate = int(sample_rate_str)
923 bit_depth = int(bit_depth_str)
924 # if no supported sample rates are defined, we accept all within max_sample_rate
925 if not supported_sample_rates and max_sample_rate and sample_rate <= max_sample_rate:
926 final_supported_sample_rates.append(sample_rate)
927 if not supported_bit_depths and max_bit_depth and bit_depth <= max_bit_depth:
928 final_supported_bit_depths.append(bit_depth)
929
930 if sample_rate not in final_supported_sample_rates:
931 continue
932 if bit_depth not in final_supported_bit_depths:
933 continue
934 options.append(option)
935 if sample_rate <= safe_max_sample_rate and bit_depth <= safe_max_bit_depth:
936 default_value.append(option_value)
937 conf_entry.options = options
938 conf_entry.default_value = default_value
939 return conf_entry
940
941
942DEFAULT_STREAM_HEADERS = {
943 "Server": APPLICATION_NAME,
944 "transferMode.dlna.org": "Streaming",
945 "contentFeatures.dlna.org": "DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000",
946 "Cache-Control": "no-cache",
947 "Pragma": "no-cache",
948 "icy-name": APPLICATION_NAME,
949}
950ICY_HEADERS = {
951 "icy-name": APPLICATION_NAME,
952 "icy-description": f"{APPLICATION_NAME} - Your personal music assistant",
953 "icy-version": "1",
954 "icy-logo": MASS_LOGO_ONLINE,
955}
956
957INTERNAL_PCM_FORMAT = AudioFormat(
958 # always prefer float32 as internal pcm format to create headroom
959 # for filters such as dsp and volume normalization
960 content_type=ContentType.PCM_F32LE,
961 bit_depth=32, # related to float32
962 sample_rate=48000, # static for flow stream, dynamic for anything else
963 channels=2, # static for flow stream, dynamic for anything else
964)
965
966# extra data / extra attributes keys
967ATTR_FAKE_POWER: Final[str] = "fake_power"
968ATTR_FAKE_VOLUME: Final[str] = "fake_volume_level"
969ATTR_FAKE_MUTE: Final[str] = "fake_volume_muted"
970ATTR_ANNOUNCEMENT_IN_PROGRESS: Final[str] = "announcement_in_progress"
971ATTR_PREVIOUS_VOLUME: Final[str] = "previous_volume"
972ATTR_LAST_POLL: Final[str] = "last_poll"
973ATTR_GROUP_MEMBERS: Final[str] = "group_members"
974ATTR_ELAPSED_TIME: Final[str] = "elapsed_time"
975ATTR_ENABLED: Final[str] = "enabled"
976ATTR_AVAILABLE: Final[str] = "available"
977ATTR_MUTE_LOCK: Final[str] = "mute_lock"
978ATTR_ACTIVE_SOURCE: Final[str] = "active_source"
979
980# Album type detection patterns
981LIVE_INDICATORS = [
982 r"\bunplugged\b",
983 r"\bin concert\b",
984 r"\bon stage\b",
985 r"\blive\b",
986]
987
988SOUNDTRACK_INDICATORS = [
989 r"\bsoundtrack\b", # Catches all soundtrack variations
990 r"\bmusic from the .* motion picture\b",
991 r"\boriginal score\b",
992 r"\bthe score\b",
993 r"\bfilm score\b",
994 r"(^|\b)score:\s*", # e.g., "Score: The Two Towers"
995 r"\bfrom the film\b",
996 r"\boriginal.*cast.*recording\b",
997]
998
999# how often we report the playback progress in the player_queues controller
1000PLAYBACK_REPORT_INTERVAL_SECONDS = 30
1001
1002# List of providers that do not use HTTP streaming
1003# but consume raw audio data over other protocols
1004# for provider domains in this list, we won't show the default
1005# http-streaming specific config options in player settings
1006NON_HTTP_PROVIDERS = ("airplay", "sendspin", "snapcast")
1007
1008# Protocol priority values (lower = more preferred)
1009PROTOCOL_PRIORITY: Final[dict[str, int]] = {
1010 "sendspin": 10,
1011 "squeezelite": 20,
1012 "chromecast": 30,
1013 "airplay": 40,
1014 "dlna": 50,
1015}
1016
1017PROTOCOL_FEATURES: Final[set[PlayerFeature]] = {
1018 # Player features that may be copied from (inactive) protocol implementations
1019 PlayerFeature.VOLUME_SET,
1020 PlayerFeature.VOLUME_MUTE,
1021 PlayerFeature.PLAY_ANNOUNCEMENT,
1022 PlayerFeature.SET_MEMBERS,
1023}
1024
1025ACTIVE_PROTOCOL_FEATURES: Final[set[PlayerFeature]] = {
1026 # Player features that may be copied from the active output protocol
1027 *PROTOCOL_FEATURES,
1028 PlayerFeature.ENQUEUE,
1029 PlayerFeature.GAPLESS_DIFFERENT_SAMPLERATE,
1030 PlayerFeature.GAPLESS_PLAYBACK,
1031 PlayerFeature.MULTI_DEVICE_DSP,
1032 PlayerFeature.PAUSE,
1033}
1034
1035DEFAULT_PROVIDERS: Final[set[tuple[str, bool]]] = {
1036 # list of providers that are setup by default once
1037 # (and they can be removed/disabled by the user if they want to)
1038 # the boolean value indicates whether it needs to be discovered on mdns
1039 ("airplay", False),
1040 ("chromecast", False),
1041 ("dlna", False),
1042 ("sonos", True),
1043 ("bluesound", True),
1044 ("heos", True),
1045}
1046