/
/
/
1"""All constants for Music Assistant."""
2
3import json
4import os
5import pathlib
6import re
7from copy import deepcopy
8from typing import Any, Final, cast
9
10from music_assistant_models.config_entries import (
11 MULTI_VALUE_SPLITTER,
12 ConfigEntry,
13 ConfigValueOption,
14)
15from music_assistant_models.constants import PLAYER_CONTROL_NONE
16from music_assistant_models.enums import (
17 ConfigEntryType,
18 ContentType,
19 CrossfadeMode,
20 MediaType,
21 PlayerFeature,
22)
23from music_assistant_models.media_items import (
24 Audiobook,
25 AudioFormat,
26 PodcastEpisode,
27 Radio,
28 SoundEffect,
29 Track,
30)
31
32APPLICATION_NAME: Final = "Music Assistant"
33
34# Type alias for items that can be added to playlists
35PlaylistPlayableItem = Track | Radio | PodcastEpisode | Audiobook | SoundEffect
36
37# Default number of tracks a music provider may return as a preview sample for a dynamic playlist
38DYNAMIC_PLAYLIST_SAMPLE_SIZE: Final[int] = 25
39
40# Corresponding MediaType enum values (must match PlaylistPlayableItem types above)
41PLAYLIST_MEDIA_TYPES: Final[tuple[MediaType, ...]] = (
42 MediaType.TRACK,
43 MediaType.RADIO,
44 MediaType.PODCAST_EPISODE,
45 MediaType.AUDIOBOOK,
46 MediaType.SOUND_EFFECT,
47)
48
49# API_SCHEMA_VERSION: bump this when adding new features to the API commands (and models)
50# or small non-breaking changes to existing commands
51API_SCHEMA_VERSION: Final[int] = 65
52
53# MIN_SCHEMA_VERSION is the minimum API schema version that the current server
54# version can work with. Only bump when there are breaking changes to existing
55# API commands or models, such as removing fields or changing field types.
56# Note that doing so will break compatibility with all clients in the field
57# (including Home Assistant) that have not yet been updated to the new API
58# schema version, so only bump this when absolutely necessary.
59MIN_SCHEMA_VERSION: Final[int] = 28
60
61
62MASS_LOGGER_NAME: Final[str] = "music_assistant"
63
64# Home Assistant system user
65HOMEASSISTANT_SYSTEM_USER: Final[str] = "homeassistant_system"
66# Port used by the internal ingress webserver for the HA integration
67INGRESS_SERVER_PORT: Final[int] = 8094
68
69UNKNOWN_ARTIST: Final[str] = "[unknown]"
70UNKNOWN_ARTIST_ID_MBID: Final[str] = "125ec42a-7229-4250-afc5-e057484327fe"
71VARIOUS_ARTISTS_NAME: Final[str] = "Various Artists"
72VARIOUS_ARTISTS_MBID: Final[str] = "89ad4ac3-39f7-470e-963a-56509c546377"
73
74
75RESOURCES_DIR: Final[pathlib.Path] = (
76 pathlib.Path(__file__).parent.resolve().joinpath("helpers/resources")
77)
78GENRE_ICONS_DIR_NAME: Final[str] = "genres"
79GENRE_MAPPING_FILE: Final[pathlib.Path] = RESOURCES_DIR.joinpath(
80 GENRE_ICONS_DIR_NAME, "genre_mapping.json"
81)
82PODCAST_GENRE_MAPPING_FILE: Final[pathlib.Path] = RESOURCES_DIR.joinpath(
83 GENRE_ICONS_DIR_NAME, "podcast_genre_mapping.json"
84)
85AUDIOBOOK_GENRE_MAPPING_FILE: Final[pathlib.Path] = RESOURCES_DIR.joinpath(
86 GENRE_ICONS_DIR_NAME, "audiobook_genre_mapping.json"
87)
88
89ANNOUNCE_ALERT_FILE: Final[str] = str(RESOURCES_DIR.joinpath("announce.mp3"))
90SILENCE_FILE: Final[str] = str(RESOURCES_DIR.joinpath("silence.mp3"))
91SILENCE_FILE_LONG: Final[str] = str(RESOURCES_DIR.joinpath("silence_long.ogg"))
92VARIOUS_ARTISTS_FANART: Final[str] = str(RESOURCES_DIR.joinpath("fallback_fanart.jpeg"))
93MASS_LOGO: Final[str] = str(RESOURCES_DIR.joinpath("logo.png"))
94
95
96# config keys
97CONF_ONBOARD_DONE: Final[str] = "onboard_done"
98CONF_SERVER_ID: Final[str] = "server_id"
99CONF_ENCRYPTION_KEY: Final[str] = "encryption_key"
100CONF_ENCRYPTION_KEY_MIGRATED: Final[str] = "encryption_key_migrated"
101CONF_NFS_SUBFOLDER_MIGRATED: Final[str] = "nfs_subfolder_migrated"
102CONF_RETIRED_LOCAL_AUDIO_CLEANED: Final[str] = "retired_local_audio_cleaned"
103CONF_IP_ADDRESS: Final[str] = "ip_address"
104CONF_PORT: Final[str] = "port"
105CONF_PROVIDERS: Final[str] = "providers"
106CONF_PLAYERS: Final[str] = "players"
107CONF_PLAYER_QUEUES: Final[str] = "player_queues"
108CONF_CORE: Final[str] = "core"
109CONF_PATH: Final[str] = "path"
110CONF_NAME: Final[str] = "name"
111CONF_USERNAME: Final[str] = "username"
112CONF_PASSWORD: Final[str] = "password"
113CONF_VOLUME_NORMALIZATION: Final[str] = "volume_normalization"
114CONF_VOLUME_NORMALIZATION_TARGET: Final[str] = "volume_normalization_target"
115CONF_PLAYER_DSP: Final[str] = "player_dsp"
116CONF_PLAYER_DSP_PRESETS: Final[str] = "player_dsp_presets"
117CONF_PLAYER_DSP_IRS: Final[str] = "player_dsp_irs"
118# subdirectory under the storage path holding convolution impulse response files
119DSP_IRS_DIRNAME: Final[str] = "dsp_irs"
120# impulse response ids are lowercased shortuuids, so plain lowercase alphanumerics;
121# validating against this keeps a caller-supplied id from escaping the storage dir
122DSP_IR_ID_RE: Final = re.compile(r"^[a-z0-9]+$")
123CONF_OUTPUT_CHANNELS: Final[str] = "output_channels"
124CONF_FLOW_MODE: Final[str] = "flow_mode"
125CONF_FLOW_MODE_SAMPLE_RATE: Final[str] = "flow_mode_sample_rate"
126CONF_LOG_LEVEL: Final[str] = "log_level"
127CONF_HIDE_GROUP_CHILDS: Final[str] = "hide_group_childs"
128CONF_CROSSFADE_DURATION: Final[str] = "crossfade_duration"
129CONF_BIND_IP: Final[str] = "bind_ip"
130CONF_BIND_PORT: Final[str] = "bind_port"
131CONF_PUBLISH_IP: Final[str] = "publish_ip"
132WILDCARD_BIND_IPS: Final[tuple[str, ...]] = ("0.0.0.0", "::")
133# Port used by the built-in Sendspin server (runs next to, not behind, the webserver)
134SENDSPIN_SERVER_PORT: Final[int] = 8927
135CONF_AUTO_PLAY: Final[str] = "auto_play"
136CONF_PLAY_MEDIA_OVERRIDES_GROUP: Final[str] = "play_media_overrides_group"
137CONF_GROUP_MEMBERS: Final[str] = "group_members"
138CONF_DYNAMIC_GROUP_MEMBERS: Final[str] = "dynamic_members"
139CONF_HIDE_IN_UI: Final[str] = "hide_in_ui"
140CONF_EXPOSE_PLAYER_TO_HA: Final[str] = "expose_player_to_ha"
141CONF_SYNC_ADJUST: Final[str] = "sync_adjust"
142CONF_TTS_PRE_ANNOUNCE: Final[str] = "tts_pre_announce"
143CONF_ANNOUNCE_VOLUME_STRATEGY: Final[str] = "announce_volume_strategy"
144CONF_ANNOUNCE_VOLUME: Final[str] = "announce_volume"
145CONF_ANNOUNCE_VOLUME_MIN: Final[str] = "announce_volume_min"
146CONF_ANNOUNCE_VOLUME_MAX: Final[str] = "announce_volume_max"
147CONF_PRE_ANNOUNCE_CHIME_URL: Final[str] = "pre_announcement_chime_url"
148CONF_ANNOUNCE_TTS_ENGINE: Final[str] = "announce_tts_engine"
149CONF_ICON: Final[str] = "icon"
150CONF_LANGUAGE: Final[str] = "language"
151CONF_SAMPLE_RATES: Final[str] = "sample_rates"
152CONF_HTTP_PROFILE: Final[str] = "http_profile"
153CONF_BYPASS_NORMALIZATION_RADIO: Final[str] = "bypass_normalization_radio"
154CONF_ENABLE_ICY_METADATA: Final[str] = "enable_icy_metadata"
155CONF_VOLUME_NORMALIZATION_RADIO: Final[str] = "volume_normalization_radio"
156CONF_VOLUME_NORMALIZATION_TRACKS: Final[str] = "volume_normalization_tracks"
157CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO: Final[str] = "volume_normalization_fixed_gain_radio"
158CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS: Final[str] = "volume_normalization_fixed_gain_tracks"
159CONF_POWER_CONTROL: Final[str] = "power_control"
160CONF_VOLUME_CONTROL: Final[str] = "volume_control"
161CONF_VOLUME_STEP: Final[str] = "volume_step"
162CONF_MUTE_CONTROL: Final[str] = "mute_control"
163CONF_MIN_VOLUME: Final[str] = "min_volume"
164CONF_MAX_VOLUME: Final[str] = "max_volume"
165CONF_PREFERRED_OUTPUT_PROTOCOL: Final[str] = "preferred_output_protocol"
166CONF_LINKED_PROTOCOL_IDS: Final[str] = "linked_protocol_ids" # cached for fast restart
167CONF_PROTOCOL_PARENT_ID: Final[str] = (
168 "protocol_parent_id" # cached native player ID for protocol player
169)
170CONF_UNDERLYING_PLAYER_ID: Final[str] = (
171 "underlying_player_id" # player this (bridge) protocol player is derived from
172)
173# Translation key of the warning a provider stores on a protocol player it considers
174# experimental. Its presence also makes the output's enable entry default to disabled;
175# a stored enabled state still wins, so the provider persists that one itself.
176CONF_PROTOCOL_EXPERIMENTAL_NOTE: Final[str] = "protocol_experimental_note"
177CONF_CACHED_ARP_MAC: Final[str] = "cached_arp_mac" # cached ARP-resolved MAC for fast restart
178CONF_REPORTED_MAC: Final[str] = "reported_mac" # original MAC reported by provider (before ARP)
179CONF_OUTPUT_CODEC: Final[str] = "output_codec"
180CONF_PREFER_WAV_FOR_LIVE_SOURCES: Final[str] = "prefer_wav_for_live_sources"
181CONF_ALLOW_AUDIO_CACHE: Final[str] = "allow_audio_cache"
182CONF_SMART_FADES_MODE: Final[str] = "smart_fades_mode" # legacy; consumed by one-time migration
183CONF_CROSSFADE_MODE: Final[str] = "crossfade_mode"
184CONF_SOCKS_URL: Final[str] = "socks_url"
185CONF_USE_SSL: Final[str] = "use_ssl"
186CONF_VERIFY_SSL: Final[str] = "verify_ssl"
187CONF_SSL_FINGERPRINT: Final[str] = "ssl_fingerprint"
188CONF_AUTH_ALLOW_SELF_REGISTRATION: Final[str] = "auth_allow_self_registration"
189CONF_ZEROCONF_INTERFACES: Final[str] = "zeroconf_interfaces"
190CONF_ENABLED: Final[str] = "enabled"
191CONF_PROTOCOL_KEY_SPLITTER: Final[str] = "||protocol||"
192CONF_PROTOCOL_CATEGORY_PREFIX: Final[str] = "protocol"
193CONF_PLUGIN_KEY_SPLITTER: Final[str] = "||plugin||"
194CONF_DEFAULT_PROVIDERS_SETUP: Final[str] = "default_providers_setup"
195CONF_BACKGROUND_SCAN_CONCURRENCY: Final[str] = "background_scan_concurrency"
196
197# Tri-state option VALUES for per-queue settings that can follow the global (queue controller)
198# default. "global" resolves to the queue-controller value (like the log_level "GLOBAL" pattern);
199# "enabled"/"disabled" force the setting on/off for that queue. (Distinct from the CONF_ENABLED key.)
200CONF_VALUE_GLOBAL: Final[str] = "global"
201CONF_VALUE_ENABLED: Final[str] = "enabled"
202CONF_VALUE_DISABLED: Final[str] = "disabled"
203
204# Sentinel option VALUE for network settings that are resolved at runtime when not explicitly
205# set (e.g. streams publish_ip / webserver base_url resolve to the server's primary IP at
206# startup), keeping the config entry defaults static/deterministic.
207CONF_VALUE_AUTO: Final[str] = "auto"
208
209
210def _default_background_scan_concurrency() -> int:
211 # Slow and steady by default: at most 2 tracks at once even on big machines. Users who
212 # want faster overnight scans can raise it (up to 16) at the cost of more CPU at night.
213 cpu_count = os.process_cpu_count() or os.cpu_count() or 4
214 if cpu_count >= 4:
215 return 2
216 return 1
217
218
219# config default values
220DEFAULT_HOST: Final[str] = "0.0.0.0"
221DEFAULT_BACKGROUND_SCAN_CONCURRENCY: Final[int] = _default_background_scan_concurrency()
222
223
224# common db tables
225DB_TABLE_PLAYLOG: Final[str] = "playlog"
226DB_TABLE_ARTISTS: Final[str] = "artists"
227DB_TABLE_ALBUMS: Final[str] = "albums"
228DB_TABLE_TRACKS: Final[str] = "tracks"
229DB_TABLE_PLAYLISTS: Final[str] = "playlists"
230DB_TABLE_RADIOS: Final[str] = "radios"
231DB_TABLE_AUDIOBOOKS: Final[str] = "audiobooks"
232DB_TABLE_PODCASTS: Final[str] = "podcasts"
233DB_TABLE_CACHE: Final[str] = "cache"
234DB_TABLE_SETTINGS: Final[str] = "settings"
235DB_TABLE_THUMBS: Final[str] = "thumbnails"
236DB_TABLE_PROVIDER_MAPPINGS: Final[str] = "provider_mappings"
237DB_TABLE_EXTERNAL_ID_LOOKUP: Final[str] = "external_id_lookup"
238DB_TABLE_ALBUM_TRACKS: Final[str] = "album_tracks"
239DB_TABLE_TRACK_ARTISTS: Final[str] = "track_artists"
240DB_TABLE_ALBUM_ARTISTS: Final[str] = "album_artists"
241DB_TABLE_AUDIOBOOK_ARTISTS: Final[str] = "audiobook_artists"
242DB_TABLE_LOUDNESS_MEASUREMENTS: Final[str] = "loudness_measurements"
243DB_TABLE_AUDIO_ANALYSIS: Final[str] = "audio_analysis"
244DB_TABLE_AUDIO_ANALYSIS_FAILURES: Final[str] = "audio_analysis_failures"
245DB_TABLE_GENRES: Final[str] = "genres"
246DB_TABLE_GENRE_MEDIA_ITEM_MAPPING: Final[str] = "genre_media_item_mapping"
247DB_TABLE_GENRE_MEDIA_ITEM_EXCLUSION: Final[str] = "genre_media_item_exclusion"
248
249# all media item tables, each of which has a search_name column
250# backed by a {table}_fts FTS5 index table
251MEDIA_ITEM_DB_TABLES: Final[tuple[str, ...]] = (
252 DB_TABLE_ARTISTS,
253 DB_TABLE_ALBUMS,
254 DB_TABLE_TRACKS,
255 DB_TABLE_PLAYLISTS,
256 DB_TABLE_RADIOS,
257 DB_TABLE_AUDIOBOOKS,
258 DB_TABLE_PODCASTS,
259 DB_TABLE_GENRES,
260)
261
262# Min fraction of a database file reclaimable before a startup VACUUM is worth running.
263VACUUM_MIN_RECLAIM_RATIO: Final[float] = 0.2
264
265# Loudness measurements at or below this value are considered unreliable:
266# ebur128 reports ~-70 LUFS when it receives near-silence or very little
267# audio (e.g. when a stream was cancelled early).
268LOUDNESS_MEASUREMENT_MIN_LUFS: Final[float] = -50.0
269
270
271def load_genre_mapping(mapping_file: pathlib.Path) -> list[dict[str, Any]]:
272 """
273 Load a default genre mapping from a JSON file.
274
275 :param mapping_file: Path to the genre mapping JSON file (music / podcast / audiobook).
276 :return: List of genre mapping dictionaries with 'genre' and 'aliases' keys.
277 :raises FileNotFoundError: If the mapping file is missing.
278 :raises ValueError: If JSON is malformed or missing required fields.
279 """
280 try:
281 content = mapping_file.read_text(encoding="utf-8")
282 data = json.loads(content)
283 except FileNotFoundError as err:
284 msg = f"Genre mapping file not found: {mapping_file}"
285 raise FileNotFoundError(msg) from err
286 except json.JSONDecodeError as err:
287 msg = f"Invalid JSON in genre mapping file: {mapping_file}"
288 raise ValueError(msg) from err
289
290 if not isinstance(data, list):
291 msg = f"Genre mapping must be a list, got {type(data).__name__}"
292 raise TypeError(msg)
293
294 for idx, entry in enumerate(data):
295 if not isinstance(entry, dict):
296 msg = f"Genre mapping entry {idx} must be a dict, got {type(entry).__name__}"
297 raise TypeError(msg)
298 if "genre" not in entry:
299 msg = f"Genre mapping entry {idx} missing required field 'genre'"
300 raise ValueError(msg)
301 if "aliases" not in entry:
302 msg = f"Genre mapping entry {idx} missing required field 'aliases'"
303 raise ValueError(msg)
304
305 return cast("list[dict[str, Any]]", data)
306
307
308DEFAULT_GENRE_MAPPING: Final[list[dict[str, Any]]] = load_genre_mapping(GENRE_MAPPING_FILE)
309DEFAULT_PODCAST_GENRE_MAPPING: Final[list[dict[str, Any]]] = load_genre_mapping(
310 PODCAST_GENRE_MAPPING_FILE
311)
312DEFAULT_AUDIOBOOK_GENRE_MAPPING: Final[list[dict[str, Any]]] = load_genre_mapping(
313 AUDIOBOOK_GENRE_MAPPING_FILE
314)
315DEFAULT_GENRES: Final[tuple[str, ...]] = tuple(entry["genre"] for entry in DEFAULT_GENRE_MAPPING)
316
317
318# all other
319MASS_LOGO_ONLINE: Final[str] = (
320 "https://raw.githubusercontent.com/music-assistant/server/refs/heads/dev/music_assistant/logo.png"
321)
322ENCRYPT_SUFFIX = "_encrypted_"
323CONFIGURABLE_CORE_CONTROLLERS = (
324 "discovery",
325 "streams",
326 "webserver",
327 "players",
328 "metadata",
329 "cache",
330 "music",
331 "player_queues",
332 "tasks",
333)
334VERBOSE_LOG_LEVEL: Final[int] = 5
335PROVIDERS_WITH_SHAREABLE_URLS = ("spotify", "qobuz", "apple_music", "deezer")
336
337
338####### REUSABLE CONFIG ENTRIES #######
339
340CONF_ENTRY_LOG_LEVEL = ConfigEntry(
341 key=CONF_LOG_LEVEL,
342 type=ConfigEntryType.STRING,
343 options=[
344 ConfigValueOption("GLOBAL"),
345 ConfigValueOption("INFO"),
346 ConfigValueOption("WARNING"),
347 ConfigValueOption("ERROR"),
348 ConfigValueOption("DEBUG"),
349 ConfigValueOption("VERBOSE"),
350 ],
351 default_value="GLOBAL",
352 advanced=True,
353 requires_reload=False, # applied dynamically via _set_logger()
354)
355
356CONF_MAX_CONCURRENT_TASKS = "max_concurrent_tasks"
357CONF_ENTRY_MAX_CONCURRENT_TASKS = ConfigEntry(
358 key=CONF_MAX_CONCURRENT_TASKS,
359 type=ConfigEntryType.INTEGER,
360 range=(1, 10),
361 default_value=2,
362 advanced=True,
363 requires_reload=False,
364)
365
366DEFAULT_PROVIDER_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,)
367DEFAULT_CORE_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,)
368
369# some reusable player config entries
370
371CONF_ENTRY_FLOW_MODE = ConfigEntry(
372 key=CONF_FLOW_MODE,
373 type=ConfigEntryType.BOOLEAN,
374 default_value=False,
375 category="protocol_generic",
376 advanced=True,
377 requires_reload=True,
378)
379
380
381FLOW_MODE_SAMPLE_RATE_SMART: Final[str] = "smart"
382FLOW_MODE_SAMPLE_RATE_BIT_PERFECT: Final[str] = "bit_perfect"
383FLOW_MODE_SAMPLE_RATE_48000: Final[str] = "48000"
384FLOW_MODE_SAMPLE_RATE_96000: Final[str] = "96000"
385FLOW_MODE_SAMPLE_RATE_HIGHEST: Final[str] = "highest"
386
387CONF_ENTRY_FLOW_MODE_SAMPLE_RATE = ConfigEntry(
388 key=CONF_FLOW_MODE_SAMPLE_RATE,
389 type=ConfigEntryType.STRING,
390 options=[
391 ConfigValueOption(FLOW_MODE_SAMPLE_RATE_SMART),
392 ConfigValueOption(FLOW_MODE_SAMPLE_RATE_BIT_PERFECT),
393 ConfigValueOption(FLOW_MODE_SAMPLE_RATE_48000),
394 ConfigValueOption(FLOW_MODE_SAMPLE_RATE_96000),
395 ConfigValueOption(FLOW_MODE_SAMPLE_RATE_HIGHEST),
396 ],
397 default_value=FLOW_MODE_SAMPLE_RATE_SMART,
398 depends_on=CONF_FLOW_MODE,
399 category="protocol_generic",
400 advanced=True,
401 requires_reload=True,
402)
403
404
405CONF_ENTRY_AUTO_PLAY = ConfigEntry(
406 key=CONF_AUTO_PLAY,
407 type=ConfigEntryType.BOOLEAN,
408 default_value=False,
409 depends_on=CONF_POWER_CONTROL,
410 depends_on_value_not="none",
411 category="player_controls",
412)
413
414CONF_ENTRY_PLAY_MEDIA_OVERRIDES_GROUP = ConfigEntry(
415 key=CONF_PLAY_MEDIA_OVERRIDES_GROUP,
416 type=ConfigEntryType.BOOLEAN,
417 default_value=True,
418 category="generic",
419 advanced=False,
420)
421
422CONF_ENTRY_MIN_VOLUME = ConfigEntry(
423 key=CONF_MIN_VOLUME,
424 type=ConfigEntryType.INTEGER,
425 range=(0, 100),
426 default_value=0,
427 category="player_controls",
428 advanced=True,
429 depends_on=CONF_VOLUME_CONTROL,
430 depends_on_value_not=PLAYER_CONTROL_NONE,
431)
432
433CONF_ENTRY_MAX_VOLUME = ConfigEntry(
434 key=CONF_MAX_VOLUME,
435 type=ConfigEntryType.INTEGER,
436 range=(0, 100),
437 default_value=100,
438 category="player_controls",
439 advanced=True,
440 depends_on=CONF_VOLUME_CONTROL,
441 depends_on_value_not=PLAYER_CONTROL_NONE,
442)
443
444CONF_ENTRY_OUTPUT_CHANNELS = ConfigEntry(
445 key=CONF_OUTPUT_CHANNELS,
446 type=ConfigEntryType.STRING,
447 options=[
448 ConfigValueOption("stereo"),
449 ConfigValueOption("left"),
450 ConfigValueOption("right"),
451 ConfigValueOption("mono"),
452 ],
453 default_value="stereo",
454 category="protocol_generic",
455 advanced=True,
456 requires_reload=True,
457)
458
459CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry(
460 key=CONF_VOLUME_NORMALIZATION_TARGET,
461 type=ConfigEntryType.INTEGER,
462 range=(-30, -5),
463 default_value=-14,
464 category="playback",
465 advanced=True,
466 requires_reload=True,
467)
468
469# Note: the crossfade_mode select entry (standard/smart) is built dynamically in the config
470# controller because its options and default depend on smart fades availability.
471
472# Crossfade duration is a global-only (queue controller) setting; it is read fresh per stream, so a
473# change applies on the next track (no reload needed — a reload of the queues core is disruptive).
474CONF_ENTRY_CROSSFADE_DURATION = ConfigEntry(
475 key=CONF_CROSSFADE_DURATION,
476 type=ConfigEntryType.INTEGER,
477 range=(1, 15),
478 default_value=8,
479 category="crossfade",
480 depends_on=CONF_CROSSFADE_MODE,
481 depends_on_value=CrossfadeMode.STANDARD_CROSSFADE.value,
482)
483
484
485CONF_ENTRY_OUTPUT_CODEC = ConfigEntry(
486 key=CONF_OUTPUT_CODEC,
487 type=ConfigEntryType.STRING,
488 default_value="flac",
489 options=[
490 ConfigValueOption("flac"),
491 ConfigValueOption("mp3"),
492 ConfigValueOption("aac"),
493 ConfigValueOption("wav"),
494 ],
495 category="protocol_generic",
496 advanced=True,
497 requires_reload=True,
498)
499
500CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3 = ConfigEntry.from_dict(
501 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "mp3"}
502)
503CONF_ENTRY_OUTPUT_CODEC_DEFAULT_AAC = ConfigEntry.from_dict(
504 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "aac"}
505)
506CONF_ENTRY_OUTPUT_CODEC_ENFORCE_MP3 = ConfigEntry.from_dict(
507 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "mp3", "hidden": True}
508)
509CONF_ENTRY_OUTPUT_CODEC_HIDDEN = ConfigEntry.from_dict(
510 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "hidden": True}
511)
512CONF_ENTRY_OUTPUT_CODEC_ENFORCE_FLAC = ConfigEntry.from_dict(
513 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "flac", "hidden": True}
514)
515
516CONF_ENTRY_PREFER_WAV_FOR_LIVE_SOURCES = ConfigEntry(
517 key=CONF_PREFER_WAV_FOR_LIVE_SOURCES,
518 type=ConfigEntryType.BOOLEAN,
519 default_value=False,
520 category="protocol_generic",
521 advanced=True,
522 requires_reload=True,
523)
524CONF_ENTRY_PREFER_WAV_FOR_LIVE_SOURCES_DEFAULT_ENABLED = ConfigEntry.from_dict(
525 {**CONF_ENTRY_PREFER_WAV_FOR_LIVE_SOURCES.to_dict(), "default_value": True}
526)
527
528
529def create_output_codec_config_entry(
530 hidden: bool = False, default_value: str = "flac"
531) -> ConfigEntry:
532 """Create output codec config entry based on player specific helpers."""
533 conf_entry = ConfigEntry.from_dict(CONF_ENTRY_OUTPUT_CODEC.to_dict())
534 conf_entry.hidden = hidden
535 conf_entry.default_value = default_value
536 return conf_entry
537
538
539CONF_ENTRY_SYNC_ADJUST = ConfigEntry(
540 key=CONF_SYNC_ADJUST,
541 type=ConfigEntryType.INTEGER,
542 range=(-500, 500),
543 default_value=0,
544 category="protocol_generic",
545 advanced=True,
546 requires_reload=True,
547)
548
549
550CONF_ENTRY_TTS_PRE_ANNOUNCE = ConfigEntry(
551 key=CONF_TTS_PRE_ANNOUNCE,
552 type=ConfigEntryType.BOOLEAN,
553 default_value=True,
554 category="announcements",
555)
556
557
558CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY = ConfigEntry(
559 key=CONF_ANNOUNCE_VOLUME_STRATEGY,
560 type=ConfigEntryType.STRING,
561 options=[
562 ConfigValueOption("absolute"),
563 ConfigValueOption("relative"),
564 ConfigValueOption("percentual"),
565 ConfigValueOption("none"),
566 ],
567 default_value="percentual",
568 category="announcements",
569)
570
571CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY_HIDDEN = ConfigEntry.from_dict(
572 {**CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.to_dict(), "hidden": True}
573)
574
575CONF_ENTRY_ANNOUNCE_VOLUME = ConfigEntry(
576 key=CONF_ANNOUNCE_VOLUME,
577 type=ConfigEntryType.INTEGER,
578 default_value=85,
579 category="announcements",
580)
581CONF_ENTRY_ANNOUNCE_VOLUME_HIDDEN = ConfigEntry.from_dict(
582 {**CONF_ENTRY_ANNOUNCE_VOLUME.to_dict(), "hidden": True}
583)
584
585CONF_ENTRY_ANNOUNCE_VOLUME_MIN = ConfigEntry(
586 key=CONF_ANNOUNCE_VOLUME_MIN,
587 type=ConfigEntryType.INTEGER,
588 default_value=15,
589 category="announcements",
590)
591CONF_ENTRY_ANNOUNCE_VOLUME_MIN_HIDDEN = ConfigEntry.from_dict(
592 {**CONF_ENTRY_ANNOUNCE_VOLUME_MIN.to_dict(), "hidden": True}
593)
594
595CONF_ENTRY_ANNOUNCE_VOLUME_MAX = ConfigEntry(
596 key=CONF_ANNOUNCE_VOLUME_MAX,
597 type=ConfigEntryType.INTEGER,
598 default_value=75,
599 category="announcements",
600)
601CONF_ENTRY_ANNOUNCE_VOLUME_MAX_HIDDEN = ConfigEntry.from_dict(
602 {**CONF_ENTRY_ANNOUNCE_VOLUME_MAX.to_dict(), "hidden": True}
603)
604
605
606HIDDEN_ANNOUNCE_VOLUME_CONFIG_ENTRIES = (
607 CONF_ENTRY_ANNOUNCE_VOLUME_HIDDEN,
608 CONF_ENTRY_ANNOUNCE_VOLUME_MIN_HIDDEN,
609 CONF_ENTRY_ANNOUNCE_VOLUME_MAX_HIDDEN,
610 CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY_HIDDEN,
611)
612
613
614CONF_ENTRY_SAMPLE_RATES = ConfigEntry(
615 key=CONF_SAMPLE_RATES,
616 type=ConfigEntryType.SPLITTED_STRING,
617 multi_value=True,
618 options=[
619 ConfigValueOption(f"44100{MULTI_VALUE_SPLITTER}16"),
620 ConfigValueOption(f"44100{MULTI_VALUE_SPLITTER}24"),
621 ConfigValueOption(f"48000{MULTI_VALUE_SPLITTER}16"),
622 ConfigValueOption(f"48000{MULTI_VALUE_SPLITTER}24"),
623 ConfigValueOption(f"88200{MULTI_VALUE_SPLITTER}16"),
624 ConfigValueOption(f"88200{MULTI_VALUE_SPLITTER}24"),
625 ConfigValueOption(f"96000{MULTI_VALUE_SPLITTER}16"),
626 ConfigValueOption(f"96000{MULTI_VALUE_SPLITTER}24"),
627 ConfigValueOption(f"176400{MULTI_VALUE_SPLITTER}16"),
628 ConfigValueOption(f"176400{MULTI_VALUE_SPLITTER}24"),
629 ConfigValueOption(f"192000{MULTI_VALUE_SPLITTER}16"),
630 ConfigValueOption(f"192000{MULTI_VALUE_SPLITTER}24"),
631 ConfigValueOption(f"352800{MULTI_VALUE_SPLITTER}16"),
632 ConfigValueOption(f"352800{MULTI_VALUE_SPLITTER}24"),
633 ConfigValueOption(f"384000{MULTI_VALUE_SPLITTER}16"),
634 ConfigValueOption(f"384000{MULTI_VALUE_SPLITTER}24"),
635 ],
636 default_value=[f"44100{MULTI_VALUE_SPLITTER}16", f"48000{MULTI_VALUE_SPLITTER}16"],
637 required=True,
638 category="protocol_generic",
639 advanced=True,
640 requires_reload=True,
641)
642
643
644CONF_ENTRY_HTTP_PROFILE = ConfigEntry(
645 key=CONF_HTTP_PROFILE,
646 type=ConfigEntryType.STRING,
647 options=[
648 ConfigValueOption("chunked"),
649 ConfigValueOption("no_content_length"),
650 ConfigValueOption("forced_content_length"),
651 ],
652 default_value="no_content_length",
653 category="protocol_generic",
654 advanced=True,
655 requires_reload=True,
656)
657
658CONF_ENTRY_HTTP_PROFILE_DEFAULT_1 = ConfigEntry.from_dict(
659 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "chunked"}
660)
661
662CONF_ENTRY_HTTP_PROFILE_DEFAULT_2 = ConfigEntry.from_dict(
663 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "no_content_length"}
664)
665CONF_ENTRY_HTTP_PROFILE_DEFAULT_3 = ConfigEntry.from_dict(
666 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "forced_content_length"}
667)
668
669CONF_ENTRY_HTTP_PROFILE_FORCED_1 = ConfigEntry.from_dict(
670 {**CONF_ENTRY_HTTP_PROFILE_DEFAULT_1.to_dict(), "hidden": True}
671)
672CONF_ENTRY_HTTP_PROFILE_FORCED_2 = ConfigEntry.from_dict(
673 {
674 **CONF_ENTRY_HTTP_PROFILE.to_dict(),
675 "default_value": "no_content_length",
676 "hidden": True,
677 }
678)
679CONF_ENTRY_HTTP_PROFILE_FORCED_3 = ConfigEntry.from_dict(
680 {
681 **CONF_ENTRY_HTTP_PROFILE.to_dict(),
682 "default_value": "forced_content_length",
683 "hidden": True,
684 }
685)
686CONF_ENTRY_HTTP_PROFILE_HIDDEN = ConfigEntry.from_dict(
687 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "hidden": True}
688)
689
690
691CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry(
692 key=CONF_ENABLE_ICY_METADATA,
693 type=ConfigEntryType.STRING,
694 options=[
695 ConfigValueOption("disabled"),
696 ConfigValueOption("basic"),
697 ConfigValueOption("full"),
698 ],
699 depends_on=CONF_FLOW_MODE,
700 depends_on_value_not=False,
701 default_value="disabled",
702 category="protocol_generic",
703 advanced=True,
704 requires_reload=True,
705)
706
707CONF_ENTRY_ENABLE_ICY_METADATA_HIDDEN = ConfigEntry.from_dict(
708 {**CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(), "hidden": True}
709)
710
711CONF_ENTRY_ICY_METADATA_HIDDEN_DISABLED = ConfigEntry.from_dict(
712 {
713 **CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(),
714 "default_value": "disabled",
715 "value": "disabled",
716 "hidden": True,
717 }
718)
719
720CONF_ENTRY_ICY_METADATA_DEFAULT_FULL = ConfigEntry.from_dict(
721 {
722 **CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(),
723 "default_value": "full",
724 }
725)
726
727CONF_ENTRY_CROSSFADE_DIFFERENT_SAMPLE_RATES = ConfigEntry(
728 key="crossfade_different_sample_rates",
729 type=ConfigEntryType.BOOLEAN,
730 default_value=True,
731 category="protocol_generic",
732 advanced=True,
733 requires_reload=True,
734 depends_on=CONF_FLOW_MODE,
735 depends_on_value_not=True,
736)
737
738CONF_ENTRY_WARN_PREVIEW = ConfigEntry(
739 key="preview_note",
740 type=ConfigEntryType.ALERT,
741 required=False,
742)
743
744CONF_ENTRY_UNOFFICIAL_PROVIDER = ConfigEntry(
745 key="unofficial_provider_note",
746 type=ConfigEntryType.ALERT,
747 required=False,
748)
749
750CONF_ENTRY_MANUAL_DISCOVERY_IPS = ConfigEntry(
751 key="manual_discovery_ip_addresses",
752 type=ConfigEntryType.STRING,
753 advanced=True,
754 default_value=[],
755 required=False,
756 multi_value=True,
757)
758
759CONF_ENTRY_LIBRARY_SYNC_ARTISTS = ConfigEntry(
760 key="library_sync_artists",
761 type=ConfigEntryType.BOOLEAN,
762 default_value=True,
763 category="sync_options",
764)
765
766
767CONF_ENTRY_ZEROCONF_INTERFACES = ConfigEntry(
768 key=CONF_ZEROCONF_INTERFACES,
769 type=ConfigEntryType.STRING,
770 options=[
771 ConfigValueOption("default"),
772 ConfigValueOption("all"),
773 ],
774 default_value="default",
775 advanced=True,
776 requires_reload=True,
777)
778CONF_ENTRY_LIBRARY_SYNC_ALBUMS = ConfigEntry(
779 key="library_sync_albums",
780 type=ConfigEntryType.BOOLEAN,
781 default_value=True,
782 category="sync_options",
783)
784CONF_ENTRY_LIBRARY_SYNC_TRACKS = ConfigEntry(
785 key="library_sync_tracks",
786 type=ConfigEntryType.BOOLEAN,
787 default_value=True,
788 category="sync_options",
789)
790CONF_ENTRY_LIBRARY_SYNC_PLAYLISTS = ConfigEntry(
791 key="library_sync_playlists",
792 type=ConfigEntryType.BOOLEAN,
793 default_value=True,
794 category="sync_options",
795)
796CONF_ENTRY_LIBRARY_SYNC_PODCASTS = ConfigEntry(
797 key="library_sync_podcasts",
798 type=ConfigEntryType.BOOLEAN,
799 default_value=True,
800 category="sync_options",
801)
802CONF_ENTRY_LIBRARY_SYNC_AUDIOBOOKS = ConfigEntry(
803 key="library_sync_audiobooks",
804 type=ConfigEntryType.BOOLEAN,
805 default_value=True,
806 category="sync_options",
807)
808CONF_ENTRY_LIBRARY_SYNC_RADIOS = ConfigEntry(
809 key="library_sync_radios",
810 type=ConfigEntryType.BOOLEAN,
811 default_value=True,
812 category="sync_options",
813)
814CONF_ENTRY_LIBRARY_SYNC_ALBUM_TRACKS = ConfigEntry(
815 key="library_sync_album_tracks",
816 type=ConfigEntryType.BOOLEAN,
817 default_value=False,
818 category="sync_options",
819)
820CONF_ENTRY_LIBRARY_SYNC_PLAYLIST_TRACKS = ConfigEntry(
821 key="library_sync_playlist_tracks",
822 type=ConfigEntryType.STRING,
823 default_value=[],
824 category="sync_options",
825 multi_value=True,
826)
827
828CONF_ENTRY_LIBRARY_SYNC_BACK = ConfigEntry(
829 key="library_sync_back",
830 type=ConfigEntryType.BOOLEAN,
831 default_value=True,
832 category="sync_options",
833)
834
835CONF_ENTRY_LIBRARY_SYNC_DELETIONS = ConfigEntry(
836 key="library_sync_deletions",
837 type=ConfigEntryType.BOOLEAN,
838 default_value=True,
839 category="sync_options",
840 advanced=True,
841)
842
843CONF_ENTRY_PLAYER_ICON = ConfigEntry(
844 key=CONF_ICON,
845 type=ConfigEntryType.ICON,
846 default_value="speaker",
847 category="generic",
848)
849
850CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict(
851 {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "speakers"}
852)
853
854
855def create_sample_rates_config_entry(
856 supported_sample_rates: list[int] | None = None,
857 supported_bit_depths: list[int] | None = None,
858 hidden: bool = False,
859 max_sample_rate: int | None = None,
860 max_bit_depth: int | None = None,
861 safe_max_sample_rate: int = 48000,
862 safe_max_bit_depth: int = 16,
863) -> ConfigEntry:
864 """Create sample rates config entry based on player specific helpers."""
865 assert CONF_ENTRY_SAMPLE_RATES.options
866 # if no supported sample rates are defined, we apply the default 44100 as only option
867 if not supported_sample_rates and max_sample_rate is None:
868 supported_sample_rates = [44100]
869 if not supported_bit_depths and max_bit_depth is None:
870 supported_bit_depths = [16]
871 final_supported_sample_rates = supported_sample_rates or []
872 final_supported_bit_depths = supported_bit_depths or []
873 conf_entry = deepcopy(CONF_ENTRY_SAMPLE_RATES)
874 conf_entry.hidden = hidden
875 options: list[ConfigValueOption] = []
876 default_value: list[str] = []
877
878 for option in CONF_ENTRY_SAMPLE_RATES.options:
879 option_value = cast("str", option.value)
880 sample_rate_str, bit_depth_str = option_value.split(MULTI_VALUE_SPLITTER, 1)
881 sample_rate = int(sample_rate_str)
882 bit_depth = int(bit_depth_str)
883 # if no supported sample rates are defined, we accept all within max_sample_rate
884 if not supported_sample_rates and max_sample_rate and sample_rate <= max_sample_rate:
885 final_supported_sample_rates.append(sample_rate)
886 if not supported_bit_depths and max_bit_depth and bit_depth <= max_bit_depth:
887 final_supported_bit_depths.append(bit_depth)
888
889 if sample_rate not in final_supported_sample_rates:
890 continue
891 if bit_depth not in final_supported_bit_depths:
892 continue
893 options.append(option)
894 if sample_rate <= safe_max_sample_rate and bit_depth <= safe_max_bit_depth:
895 default_value.append(option_value)
896 conf_entry.options = options
897 conf_entry.default_value = default_value
898 return conf_entry
899
900
901DEFAULT_STREAM_HEADERS = {
902 "Server": APPLICATION_NAME,
903 "transferMode.dlna.org": "Streaming",
904 "Cache-Control": "no-cache",
905 "Pragma": "no-cache",
906 "Accept-Ranges": "none",
907 "Connection": "close",
908 "icy-name": APPLICATION_NAME,
909}
910
911# DLNA contentFeatures header values for different stream types.
912# ORG_OP=00: no time-seek, no byte-seek (we encode on-the-fly, unknown size).
913# ORG_FLAGS bit layout (hex, first 8 chars of 32-char field):
914# bit 31 (0x80000000): Sender Paced - server controls data rate
915# bit 24 (0x01000000): Streaming Transfer Mode
916# bit 22 (0x00400000): Background Transfer Mode
917# bit 21 (0x00200000): HTTP Connection Stalling permitted
918# bit 20 (0x00100000): DLNA V1.5
919#
920# Bufferable streams (tracks, flow mode): player may buffer aggressively.
921# Flags: 0x01700000 = Streaming + Background + Connection Stalling + V1.5
922DLNA_CONTENT_FEATURES = "DLNA.ORG_OP=00;DLNA.ORG_FLAGS=01700000000000000000000000000000"
923# Realtime streams (radio, plugin sources): server controls data rate,
924# player should not try to buffer ahead faster than the server delivers.
925# Flags: 0x81700000 = Sender Paced + Streaming + Background + Connection Stalling + V1.5
926DLNA_CONTENT_FEATURES_REALTIME = "DLNA.ORG_OP=00;DLNA.ORG_FLAGS=81700000000000000000000000000000"
927ICY_HEADERS = {
928 "icy-name": APPLICATION_NAME,
929 "icy-description": f"{APPLICATION_NAME} - Your personal music assistant",
930 "icy-version": "1",
931 "icy-logo": MASS_LOGO_ONLINE,
932}
933
934INTERNAL_PCM_FORMAT = AudioFormat(
935 # always prefer float32 as internal pcm format to create headroom
936 # for filters such as dsp and volume normalization
937 content_type=ContentType.PCM_F32LE,
938 bit_depth=32, # related to float32
939 sample_rate=48000, # static for flow stream, dynamic for anything else
940 channels=2, # static for flow stream, dynamic for anything else
941)
942
943# Seconds without a new chunk before the source is treated as stalled (above ffmpeg's ~15s reconnect window).
944STREAM_STALL_TIMEOUT: Final[int] = 20
945# Longer budget for the first chunk to allow for connect + probe.
946STREAM_START_TIMEOUT: Final[int] = 30
947
948# extra data / extra attributes keys
949ATTR_FAKE_POWER: Final[str] = "fake_power"
950ATTR_FAKE_VOLUME: Final[str] = "fake_volume_level"
951ATTR_FAKE_MUTE: Final[str] = "fake_volume_muted"
952ATTR_ANNOUNCEMENT_IN_PROGRESS: Final[str] = "announcement_in_progress"
953ATTR_PREVIOUS_VOLUME: Final[str] = "previous_volume"
954ATTR_LAST_POLL: Final[str] = "last_poll"
955ATTR_GROUP_MEMBERS: Final[str] = "group_members"
956ATTR_GROUP_VOLUME_SNAPSHOT: Final[str] = "group_volume_snapshot"
957ATTR_VOLUME_TARGET: Final[str] = "volume_target"
958ATTR_ENABLED: Final[str] = "enabled"
959ATTR_AVAILABLE: Final[str] = "available"
960ATTR_POWERED: Final[str] = "powered"
961ATTR_MUTE_LOCK: Final[str] = "mute_lock"
962ATTR_ACTIVE_SOURCE: Final[str] = "active_source"
963ATTR_SUPPORTED_FEATURES: Final[str] = "supported_features"
964ATTR_MUTE_CONTROL: Final[str] = "mute_control"
965ATTR_VOLUME_CONTROL: Final[str] = "volume_control"
966ATTR_POWER_CONTROL: Final[str] = "power_control"
967ATTR_PLAY_ACTION_IN_PROGRESS: Final[str] = "play_action_in_progress"
968
969# Album type detection patterns
970LIVE_INDICATORS = [
971 r"\bunplugged\b",
972 r"\bin concert\b",
973 r"\bon stage\b",
974 r"\blive\b",
975]
976
977SOUNDTRACK_INDICATORS = [
978 r"\bsoundtrack\b", # Catches all soundtrack variations
979 r"\bmusic from the .* motion picture\b",
980 r"\boriginal score\b",
981 r"\bthe score\b",
982 r"\bfilm score\b",
983 r"(^|\b)score:\s*", # e.g., "Score: The Two Towers"
984 r"\bfrom the film\b",
985 r"\boriginal.*cast.*recording\b",
986]
987
988# how often we report the playback progress in the player_queues controller
989PLAYBACK_REPORT_INTERVAL_SECONDS = 30
990
991# List of providers that do not use HTTP streaming
992# but consume raw audio data over other protocols
993# for provider domains in this list, we won't show the default
994# http-streaming specific config options in player settings
995NON_HTTP_PROVIDERS = ("airplay", "sendspin", "snapcast")
996
997# Protocol priority values (lower = more preferred)
998PROTOCOL_PRIORITY: Final[dict[str, int]] = {
999 "airplay": 10,
1000 "squeezelite": 20,
1001 "chromecast": 30,
1002 "sendspin": 40,
1003 "dlna": 50,
1004}
1005
1006PROTOCOL_FEATURES: Final[set[PlayerFeature]] = {
1007 # Player features that may be copied from (inactive) protocol implementations
1008 PlayerFeature.PLAY_ANNOUNCEMENT,
1009 PlayerFeature.SET_MEMBERS,
1010}
1011
1012ACTIVE_PROTOCOL_FEATURES: Final[set[PlayerFeature]] = {
1013 # Player features that may be copied from the active output protocol
1014 *PROTOCOL_FEATURES,
1015 PlayerFeature.ENQUEUE,
1016 PlayerFeature.GAPLESS_PLAYBACK,
1017 PlayerFeature.MULTI_DEVICE_DSP,
1018 PlayerFeature.PAUSE,
1019}
1020
1021PLAYER_CONTROL_PROTOCOL: Final[str] = "follow_protocol"
1022DEFAULT_PROVIDERS: Final[set[tuple[str, bool]]] = {
1023 # list of providers that are setup by default once
1024 # (and they can be removed/disabled by the user if they want to)
1025 # the boolean value indicates whether it needs to be discovered on mdns
1026 ("airplay", False),
1027 ("chromecast", False),
1028 ("dlna", False),
1029 ("sonos", True),
1030 ("bluesound", True),
1031 ("heos", True),
1032 ("wiim", True),
1033 ("party", False),
1034 # smart_fades gates on system requirements (RAM/CPU) in its own setup(); an
1035 # under-spec host has the auto-created config removed again at load time.
1036 ("smart_fades", False),
1037 ("lastfm_recommendations", False),
1038 ("playlist_metadata", False),
1039 # ambient_sounds provides out-of-the-box sound effects (e.g. for the queue
1040 # audio overlay feature) at zero resource cost until actually used
1041 ("ambient_sounds", False),
1042}
1043
1044# Seconds an external source may sit paused before we consider its session ended.
1045# Devices keep a source like Spotify Connect loaded and paused indefinitely, also once
1046# the app released the speaker, and offer nothing that tells an abandoned session apart
1047# from a real pause - so time is the only signal left. Kept generous because this is
1048# what a real pause is given before we stop presenting the source as resumable.
1049EXTERNAL_PAUSE_IDLE_TIMEOUT: Final[int] = 60
1050
1051EXTERNAL_SOURCES: Final[set[str]] = {
1052 # list of sources that are definitely considered "external"
1053 # values are matched case-insensitive against the player's active_source
1054 # streaming services / connect sources
1055 "spotify",
1056 "spotify_connect",
1057 "spotify connect",
1058 "apple_music",
1059 "apple music",
1060 "tidal",
1061 "tidal_connect",
1062 "tidal connect",
1063 "qobuz",
1064 "deezer",
1065 "amazon_music",
1066 "amazon music",
1067 "pandora",
1068 "iheartradio",
1069 "napster",
1070 "rhapsody",
1071 "siriusxm",
1072 "soundcloud",
1073 "tunein",
1074 "tune in",
1075 "radioparadise",
1076 "radio paradise",
1077 "radiko",
1078 "juke",
1079 "alexa",
1080 "radio",
1081 "airplay",
1082 "chromecast",
1083 # bluetooth (bluesound, musiccast)
1084 "bluetooth",
1085 "bluetooth audio",
1086 "bluetooth_audio",
1087 # physical/analog inputs (sonos, heos, musiccast, demo)
1088 "line-in",
1089 "linein",
1090 "line in",
1091 "line_in",
1092 "aux",
1093 "tuner",
1094 # tv / hdmi (sonos, bluesound)
1095 "tv",
1096 "tv input",
1097 "hdmi arc",
1098 # local/usb playback on device (musiccast)
1099 "usb",
1100 # external (hass_players)
1101 "external",
1102}
1103
1104COLLECTION_ITEM_ID_SEPARATOR = "___"
1105