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