music-assistant-server

40.3 KBPY
constants.py
40.3 KB1,054 lines • python
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