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