/
/
/
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] = 64
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_ENFORCE_MP3 = ConfigEntry.from_dict(
504 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "mp3", "hidden": True}
505)
506CONF_ENTRY_OUTPUT_CODEC_HIDDEN = ConfigEntry.from_dict(
507 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "hidden": True}
508)
509CONF_ENTRY_OUTPUT_CODEC_ENFORCE_FLAC = ConfigEntry.from_dict(
510 {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "flac", "hidden": True}
511)
512
513CONF_ENTRY_PREFER_WAV_FOR_LIVE_SOURCES = ConfigEntry(
514 key=CONF_PREFER_WAV_FOR_LIVE_SOURCES,
515 type=ConfigEntryType.BOOLEAN,
516 default_value=False,
517 category="protocol_generic",
518 advanced=True,
519 requires_reload=True,
520)
521CONF_ENTRY_PREFER_WAV_FOR_LIVE_SOURCES_DEFAULT_ENABLED = ConfigEntry.from_dict(
522 {**CONF_ENTRY_PREFER_WAV_FOR_LIVE_SOURCES.to_dict(), "default_value": True}
523)
524
525
526def create_output_codec_config_entry(
527 hidden: bool = False, default_value: str = "flac"
528) -> ConfigEntry:
529 """Create output codec config entry based on player specific helpers."""
530 conf_entry = ConfigEntry.from_dict(CONF_ENTRY_OUTPUT_CODEC.to_dict())
531 conf_entry.hidden = hidden
532 conf_entry.default_value = default_value
533 return conf_entry
534
535
536CONF_ENTRY_SYNC_ADJUST = ConfigEntry(
537 key=CONF_SYNC_ADJUST,
538 type=ConfigEntryType.INTEGER,
539 range=(-500, 500),
540 default_value=0,
541 category="protocol_generic",
542 advanced=True,
543 requires_reload=True,
544)
545
546
547CONF_ENTRY_TTS_PRE_ANNOUNCE = ConfigEntry(
548 key=CONF_TTS_PRE_ANNOUNCE,
549 type=ConfigEntryType.BOOLEAN,
550 default_value=True,
551 category="announcements",
552)
553
554
555CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY = ConfigEntry(
556 key=CONF_ANNOUNCE_VOLUME_STRATEGY,
557 type=ConfigEntryType.STRING,
558 options=[
559 ConfigValueOption("absolute"),
560 ConfigValueOption("relative"),
561 ConfigValueOption("percentual"),
562 ConfigValueOption("none"),
563 ],
564 default_value="percentual",
565 category="announcements",
566)
567
568CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY_HIDDEN = ConfigEntry.from_dict(
569 {**CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY.to_dict(), "hidden": True}
570)
571
572CONF_ENTRY_ANNOUNCE_VOLUME = ConfigEntry(
573 key=CONF_ANNOUNCE_VOLUME,
574 type=ConfigEntryType.INTEGER,
575 default_value=85,
576 category="announcements",
577)
578CONF_ENTRY_ANNOUNCE_VOLUME_HIDDEN = ConfigEntry.from_dict(
579 {**CONF_ENTRY_ANNOUNCE_VOLUME.to_dict(), "hidden": True}
580)
581
582CONF_ENTRY_ANNOUNCE_VOLUME_MIN = ConfigEntry(
583 key=CONF_ANNOUNCE_VOLUME_MIN,
584 type=ConfigEntryType.INTEGER,
585 default_value=15,
586 category="announcements",
587)
588CONF_ENTRY_ANNOUNCE_VOLUME_MIN_HIDDEN = ConfigEntry.from_dict(
589 {**CONF_ENTRY_ANNOUNCE_VOLUME_MIN.to_dict(), "hidden": True}
590)
591
592CONF_ENTRY_ANNOUNCE_VOLUME_MAX = ConfigEntry(
593 key=CONF_ANNOUNCE_VOLUME_MAX,
594 type=ConfigEntryType.INTEGER,
595 default_value=75,
596 category="announcements",
597)
598CONF_ENTRY_ANNOUNCE_VOLUME_MAX_HIDDEN = ConfigEntry.from_dict(
599 {**CONF_ENTRY_ANNOUNCE_VOLUME_MAX.to_dict(), "hidden": True}
600)
601
602
603HIDDEN_ANNOUNCE_VOLUME_CONFIG_ENTRIES = (
604 CONF_ENTRY_ANNOUNCE_VOLUME_HIDDEN,
605 CONF_ENTRY_ANNOUNCE_VOLUME_MIN_HIDDEN,
606 CONF_ENTRY_ANNOUNCE_VOLUME_MAX_HIDDEN,
607 CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY_HIDDEN,
608)
609
610
611CONF_ENTRY_SAMPLE_RATES = ConfigEntry(
612 key=CONF_SAMPLE_RATES,
613 type=ConfigEntryType.SPLITTED_STRING,
614 multi_value=True,
615 options=[
616 ConfigValueOption(f"44100{MULTI_VALUE_SPLITTER}16"),
617 ConfigValueOption(f"44100{MULTI_VALUE_SPLITTER}24"),
618 ConfigValueOption(f"48000{MULTI_VALUE_SPLITTER}16"),
619 ConfigValueOption(f"48000{MULTI_VALUE_SPLITTER}24"),
620 ConfigValueOption(f"88200{MULTI_VALUE_SPLITTER}16"),
621 ConfigValueOption(f"88200{MULTI_VALUE_SPLITTER}24"),
622 ConfigValueOption(f"96000{MULTI_VALUE_SPLITTER}16"),
623 ConfigValueOption(f"96000{MULTI_VALUE_SPLITTER}24"),
624 ConfigValueOption(f"176400{MULTI_VALUE_SPLITTER}16"),
625 ConfigValueOption(f"176400{MULTI_VALUE_SPLITTER}24"),
626 ConfigValueOption(f"192000{MULTI_VALUE_SPLITTER}16"),
627 ConfigValueOption(f"192000{MULTI_VALUE_SPLITTER}24"),
628 ConfigValueOption(f"352800{MULTI_VALUE_SPLITTER}16"),
629 ConfigValueOption(f"352800{MULTI_VALUE_SPLITTER}24"),
630 ConfigValueOption(f"384000{MULTI_VALUE_SPLITTER}16"),
631 ConfigValueOption(f"384000{MULTI_VALUE_SPLITTER}24"),
632 ],
633 default_value=[f"44100{MULTI_VALUE_SPLITTER}16", f"48000{MULTI_VALUE_SPLITTER}16"],
634 required=True,
635 category="protocol_generic",
636 advanced=True,
637 requires_reload=True,
638)
639
640
641CONF_ENTRY_HTTP_PROFILE = ConfigEntry(
642 key=CONF_HTTP_PROFILE,
643 type=ConfigEntryType.STRING,
644 options=[
645 ConfigValueOption("chunked"),
646 ConfigValueOption("no_content_length"),
647 ConfigValueOption("forced_content_length"),
648 ],
649 default_value="no_content_length",
650 category="protocol_generic",
651 advanced=True,
652 requires_reload=True,
653)
654
655CONF_ENTRY_HTTP_PROFILE_DEFAULT_1 = ConfigEntry.from_dict(
656 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "chunked"}
657)
658
659CONF_ENTRY_HTTP_PROFILE_DEFAULT_2 = ConfigEntry.from_dict(
660 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "no_content_length"}
661)
662CONF_ENTRY_HTTP_PROFILE_DEFAULT_3 = ConfigEntry.from_dict(
663 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "forced_content_length"}
664)
665
666CONF_ENTRY_HTTP_PROFILE_FORCED_1 = ConfigEntry.from_dict(
667 {**CONF_ENTRY_HTTP_PROFILE_DEFAULT_1.to_dict(), "hidden": True}
668)
669CONF_ENTRY_HTTP_PROFILE_FORCED_2 = ConfigEntry.from_dict(
670 {
671 **CONF_ENTRY_HTTP_PROFILE.to_dict(),
672 "default_value": "no_content_length",
673 "hidden": True,
674 }
675)
676CONF_ENTRY_HTTP_PROFILE_FORCED_3 = ConfigEntry.from_dict(
677 {
678 **CONF_ENTRY_HTTP_PROFILE.to_dict(),
679 "default_value": "forced_content_length",
680 "hidden": True,
681 }
682)
683CONF_ENTRY_HTTP_PROFILE_HIDDEN = ConfigEntry.from_dict(
684 {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "hidden": True}
685)
686
687
688CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry(
689 key=CONF_ENABLE_ICY_METADATA,
690 type=ConfigEntryType.STRING,
691 options=[
692 ConfigValueOption("disabled"),
693 ConfigValueOption("basic"),
694 ConfigValueOption("full"),
695 ],
696 depends_on=CONF_FLOW_MODE,
697 depends_on_value_not=False,
698 default_value="disabled",
699 category="protocol_generic",
700 advanced=True,
701 requires_reload=True,
702)
703
704CONF_ENTRY_ENABLE_ICY_METADATA_HIDDEN = ConfigEntry.from_dict(
705 {**CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(), "hidden": True}
706)
707
708CONF_ENTRY_ICY_METADATA_HIDDEN_DISABLED = ConfigEntry.from_dict(
709 {
710 **CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(),
711 "default_value": "disabled",
712 "value": "disabled",
713 "hidden": True,
714 }
715)
716
717CONF_ENTRY_ICY_METADATA_DEFAULT_FULL = ConfigEntry.from_dict(
718 {
719 **CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(),
720 "default_value": "full",
721 }
722)
723
724CONF_ENTRY_CROSSFADE_DIFFERENT_SAMPLE_RATES = ConfigEntry(
725 key="crossfade_different_sample_rates",
726 type=ConfigEntryType.BOOLEAN,
727 default_value=True,
728 category="protocol_generic",
729 advanced=True,
730 requires_reload=True,
731 depends_on=CONF_FLOW_MODE,
732 depends_on_value_not=True,
733)
734
735CONF_ENTRY_WARN_PREVIEW = ConfigEntry(
736 key="preview_note",
737 type=ConfigEntryType.ALERT,
738 required=False,
739)
740
741CONF_ENTRY_UNOFFICIAL_PROVIDER = ConfigEntry(
742 key="unofficial_provider_note",
743 type=ConfigEntryType.ALERT,
744 required=False,
745)
746
747CONF_ENTRY_MANUAL_DISCOVERY_IPS = ConfigEntry(
748 key="manual_discovery_ip_addresses",
749 type=ConfigEntryType.STRING,
750 advanced=True,
751 default_value=[],
752 required=False,
753 multi_value=True,
754)
755
756CONF_ENTRY_LIBRARY_SYNC_ARTISTS = ConfigEntry(
757 key="library_sync_artists",
758 type=ConfigEntryType.BOOLEAN,
759 default_value=True,
760 category="sync_options",
761)
762
763
764CONF_ENTRY_ZEROCONF_INTERFACES = ConfigEntry(
765 key=CONF_ZEROCONF_INTERFACES,
766 type=ConfigEntryType.STRING,
767 options=[
768 ConfigValueOption("default"),
769 ConfigValueOption("all"),
770 ],
771 default_value="default",
772 advanced=True,
773 requires_reload=True,
774)
775CONF_ENTRY_LIBRARY_SYNC_ALBUMS = ConfigEntry(
776 key="library_sync_albums",
777 type=ConfigEntryType.BOOLEAN,
778 default_value=True,
779 category="sync_options",
780)
781CONF_ENTRY_LIBRARY_SYNC_TRACKS = ConfigEntry(
782 key="library_sync_tracks",
783 type=ConfigEntryType.BOOLEAN,
784 default_value=True,
785 category="sync_options",
786)
787CONF_ENTRY_LIBRARY_SYNC_PLAYLISTS = ConfigEntry(
788 key="library_sync_playlists",
789 type=ConfigEntryType.BOOLEAN,
790 default_value=True,
791 category="sync_options",
792)
793CONF_ENTRY_LIBRARY_SYNC_PODCASTS = ConfigEntry(
794 key="library_sync_podcasts",
795 type=ConfigEntryType.BOOLEAN,
796 default_value=True,
797 category="sync_options",
798)
799CONF_ENTRY_LIBRARY_SYNC_AUDIOBOOKS = ConfigEntry(
800 key="library_sync_audiobooks",
801 type=ConfigEntryType.BOOLEAN,
802 default_value=True,
803 category="sync_options",
804)
805CONF_ENTRY_LIBRARY_SYNC_RADIOS = ConfigEntry(
806 key="library_sync_radios",
807 type=ConfigEntryType.BOOLEAN,
808 default_value=True,
809 category="sync_options",
810)
811CONF_ENTRY_LIBRARY_SYNC_ALBUM_TRACKS = ConfigEntry(
812 key="library_sync_album_tracks",
813 type=ConfigEntryType.BOOLEAN,
814 default_value=False,
815 category="sync_options",
816)
817CONF_ENTRY_LIBRARY_SYNC_PLAYLIST_TRACKS = ConfigEntry(
818 key="library_sync_playlist_tracks",
819 type=ConfigEntryType.STRING,
820 default_value=[],
821 category="sync_options",
822 multi_value=True,
823)
824
825CONF_ENTRY_LIBRARY_SYNC_BACK = ConfigEntry(
826 key="library_sync_back",
827 type=ConfigEntryType.BOOLEAN,
828 default_value=True,
829 category="sync_options",
830)
831
832CONF_ENTRY_LIBRARY_SYNC_DELETIONS = ConfigEntry(
833 key="library_sync_deletions",
834 type=ConfigEntryType.BOOLEAN,
835 default_value=True,
836 category="sync_options",
837 advanced=True,
838)
839
840CONF_ENTRY_PLAYER_ICON = ConfigEntry(
841 key=CONF_ICON,
842 type=ConfigEntryType.ICON,
843 default_value="speaker",
844 category="generic",
845)
846
847CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict(
848 {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "speakers"}
849)
850
851
852def create_sample_rates_config_entry(
853 supported_sample_rates: list[int] | None = None,
854 supported_bit_depths: list[int] | None = None,
855 hidden: bool = False,
856 max_sample_rate: int | None = None,
857 max_bit_depth: int | None = None,
858 safe_max_sample_rate: int = 48000,
859 safe_max_bit_depth: int = 16,
860) -> ConfigEntry:
861 """Create sample rates config entry based on player specific helpers."""
862 assert CONF_ENTRY_SAMPLE_RATES.options
863 # if no supported sample rates are defined, we apply the default 44100 as only option
864 if not supported_sample_rates and max_sample_rate is None:
865 supported_sample_rates = [44100]
866 if not supported_bit_depths and max_bit_depth is None:
867 supported_bit_depths = [16]
868 final_supported_sample_rates = supported_sample_rates or []
869 final_supported_bit_depths = supported_bit_depths or []
870 conf_entry = deepcopy(CONF_ENTRY_SAMPLE_RATES)
871 conf_entry.hidden = hidden
872 options: list[ConfigValueOption] = []
873 default_value: list[str] = []
874
875 for option in CONF_ENTRY_SAMPLE_RATES.options:
876 option_value = cast("str", option.value)
877 sample_rate_str, bit_depth_str = option_value.split(MULTI_VALUE_SPLITTER, 1)
878 sample_rate = int(sample_rate_str)
879 bit_depth = int(bit_depth_str)
880 # if no supported sample rates are defined, we accept all within max_sample_rate
881 if not supported_sample_rates and max_sample_rate and sample_rate <= max_sample_rate:
882 final_supported_sample_rates.append(sample_rate)
883 if not supported_bit_depths and max_bit_depth and bit_depth <= max_bit_depth:
884 final_supported_bit_depths.append(bit_depth)
885
886 if sample_rate not in final_supported_sample_rates:
887 continue
888 if bit_depth not in final_supported_bit_depths:
889 continue
890 options.append(option)
891 if sample_rate <= safe_max_sample_rate and bit_depth <= safe_max_bit_depth:
892 default_value.append(option_value)
893 conf_entry.options = options
894 conf_entry.default_value = default_value
895 return conf_entry
896
897
898DEFAULT_STREAM_HEADERS = {
899 "Server": APPLICATION_NAME,
900 "transferMode.dlna.org": "Streaming",
901 "Cache-Control": "no-cache",
902 "Pragma": "no-cache",
903 "Accept-Ranges": "none",
904 "Connection": "close",
905 "icy-name": APPLICATION_NAME,
906}
907
908# DLNA contentFeatures header values for different stream types.
909# ORG_OP=00: no time-seek, no byte-seek (we encode on-the-fly, unknown size).
910# ORG_FLAGS bit layout (hex, first 8 chars of 32-char field):
911# bit 31 (0x80000000): Sender Paced - server controls data rate
912# bit 24 (0x01000000): Streaming Transfer Mode
913# bit 22 (0x00400000): Background Transfer Mode
914# bit 21 (0x00200000): HTTP Connection Stalling permitted
915# bit 20 (0x00100000): DLNA V1.5
916#
917# Bufferable streams (tracks, flow mode): player may buffer aggressively.
918# Flags: 0x01700000 = Streaming + Background + Connection Stalling + V1.5
919DLNA_CONTENT_FEATURES = "DLNA.ORG_OP=00;DLNA.ORG_FLAGS=01700000000000000000000000000000"
920# Realtime streams (radio, plugin sources): server controls data rate,
921# player should not try to buffer ahead faster than the server delivers.
922# Flags: 0x81700000 = Sender Paced + Streaming + Background + Connection Stalling + V1.5
923DLNA_CONTENT_FEATURES_REALTIME = "DLNA.ORG_OP=00;DLNA.ORG_FLAGS=81700000000000000000000000000000"
924ICY_HEADERS = {
925 "icy-name": APPLICATION_NAME,
926 "icy-description": f"{APPLICATION_NAME} - Your personal music assistant",
927 "icy-version": "1",
928 "icy-logo": MASS_LOGO_ONLINE,
929}
930
931INTERNAL_PCM_FORMAT = AudioFormat(
932 # always prefer float32 as internal pcm format to create headroom
933 # for filters such as dsp and volume normalization
934 content_type=ContentType.PCM_F32LE,
935 bit_depth=32, # related to float32
936 sample_rate=48000, # static for flow stream, dynamic for anything else
937 channels=2, # static for flow stream, dynamic for anything else
938)
939
940# Seconds without a new chunk before the source is treated as stalled (above ffmpeg's ~15s reconnect window).
941STREAM_STALL_TIMEOUT: Final[int] = 20
942# Longer budget for the first chunk to allow for connect + probe.
943STREAM_START_TIMEOUT: Final[int] = 30
944
945# extra data / extra attributes keys
946ATTR_FAKE_POWER: Final[str] = "fake_power"
947ATTR_FAKE_VOLUME: Final[str] = "fake_volume_level"
948ATTR_FAKE_MUTE: Final[str] = "fake_volume_muted"
949ATTR_ANNOUNCEMENT_IN_PROGRESS: Final[str] = "announcement_in_progress"
950ATTR_PREVIOUS_VOLUME: Final[str] = "previous_volume"
951ATTR_LAST_POLL: Final[str] = "last_poll"
952ATTR_GROUP_MEMBERS: Final[str] = "group_members"
953ATTR_GROUP_VOLUME_SNAPSHOT: Final[str] = "group_volume_snapshot"
954ATTR_VOLUME_TARGET: Final[str] = "volume_target"
955ATTR_ENABLED: Final[str] = "enabled"
956ATTR_AVAILABLE: Final[str] = "available"
957ATTR_POWERED: Final[str] = "powered"
958ATTR_MUTE_LOCK: Final[str] = "mute_lock"
959ATTR_ACTIVE_SOURCE: Final[str] = "active_source"
960ATTR_SUPPORTED_FEATURES: Final[str] = "supported_features"
961ATTR_MUTE_CONTROL: Final[str] = "mute_control"
962ATTR_VOLUME_CONTROL: Final[str] = "volume_control"
963ATTR_POWER_CONTROL: Final[str] = "power_control"
964ATTR_PLAY_ACTION_IN_PROGRESS: Final[str] = "play_action_in_progress"
965
966# Album type detection patterns
967LIVE_INDICATORS = [
968 r"\bunplugged\b",
969 r"\bin concert\b",
970 r"\bon stage\b",
971 r"\blive\b",
972]
973
974SOUNDTRACK_INDICATORS = [
975 r"\bsoundtrack\b", # Catches all soundtrack variations
976 r"\bmusic from the .* motion picture\b",
977 r"\boriginal score\b",
978 r"\bthe score\b",
979 r"\bfilm score\b",
980 r"(^|\b)score:\s*", # e.g., "Score: The Two Towers"
981 r"\bfrom the film\b",
982 r"\boriginal.*cast.*recording\b",
983]
984
985# how often we report the playback progress in the player_queues controller
986PLAYBACK_REPORT_INTERVAL_SECONDS = 30
987
988# List of providers that do not use HTTP streaming
989# but consume raw audio data over other protocols
990# for provider domains in this list, we won't show the default
991# http-streaming specific config options in player settings
992NON_HTTP_PROVIDERS = ("airplay", "sendspin", "snapcast")
993
994# Protocol priority values (lower = more preferred)
995PROTOCOL_PRIORITY: Final[dict[str, int]] = {
996 "airplay": 10,
997 "squeezelite": 20,
998 "chromecast": 30,
999 "sendspin": 40,
1000 "dlna": 50,
1001}
1002
1003PROTOCOL_FEATURES: Final[set[PlayerFeature]] = {
1004 # Player features that may be copied from (inactive) protocol implementations
1005 PlayerFeature.PLAY_ANNOUNCEMENT,
1006 PlayerFeature.SET_MEMBERS,
1007}
1008
1009ACTIVE_PROTOCOL_FEATURES: Final[set[PlayerFeature]] = {
1010 # Player features that may be copied from the active output protocol
1011 *PROTOCOL_FEATURES,
1012 PlayerFeature.ENQUEUE,
1013 PlayerFeature.GAPLESS_PLAYBACK,
1014 PlayerFeature.MULTI_DEVICE_DSP,
1015 PlayerFeature.PAUSE,
1016}
1017
1018PLAYER_CONTROL_PROTOCOL: Final[str] = "follow_protocol"
1019DEFAULT_PROVIDERS: Final[set[tuple[str, bool]]] = {
1020 # list of providers that are setup by default once
1021 # (and they can be removed/disabled by the user if they want to)
1022 # the boolean value indicates whether it needs to be discovered on mdns
1023 ("airplay", False),
1024 ("chromecast", False),
1025 ("dlna", False),
1026 ("sonos", True),
1027 ("bluesound", True),
1028 ("heos", True),
1029 ("wiim", True),
1030 ("party", False),
1031 # smart_fades gates on system requirements (RAM/CPU) in its own setup(); an
1032 # under-spec host has the auto-created config removed again at load time.
1033 ("smart_fades", False),
1034 ("lastfm_recommendations", False),
1035 ("playlist_metadata", False),
1036 # ambient_sounds provides out-of-the-box sound effects (e.g. for the queue
1037 # audio overlay feature) at zero resource cost until actually used
1038 ("ambient_sounds", False),
1039}
1040
1041# Seconds an external source may sit paused before we consider its session ended.
1042# Devices keep a source like Spotify Connect loaded and paused indefinitely, also once
1043# the app released the speaker, and offer nothing that tells an abandoned session apart
1044# from a real pause - so time is the only signal left. Kept generous because this is
1045# what a real pause is given before we stop presenting the source as resumable.
1046EXTERNAL_PAUSE_IDLE_TIMEOUT: Final[int] = 60
1047
1048EXTERNAL_SOURCES: Final[set[str]] = {
1049 # list of sources that are definitely considered "external"
1050 # values are matched case-insensitive against the player's active_source
1051 # streaming services / connect sources
1052 "spotify",
1053 "spotify_connect",
1054 "spotify connect",
1055 "apple_music",
1056 "apple music",
1057 "tidal",
1058 "tidal_connect",
1059 "tidal connect",
1060 "qobuz",
1061 "deezer",
1062 "amazon_music",
1063 "amazon music",
1064 "pandora",
1065 "iheartradio",
1066 "napster",
1067 "rhapsody",
1068 "siriusxm",
1069 "soundcloud",
1070 "tunein",
1071 "tune in",
1072 "radioparadise",
1073 "radio paradise",
1074 "radiko",
1075 "juke",
1076 "alexa",
1077 "radio",
1078 "airplay",
1079 "chromecast",
1080 # bluetooth (bluesound, musiccast)
1081 "bluetooth",
1082 "bluetooth audio",
1083 "bluetooth_audio",
1084 # physical/analog inputs (sonos, heos, musiccast, demo)
1085 "line-in",
1086 "linein",
1087 "line in",
1088 "line_in",
1089 "aux",
1090 "tuner",
1091 # tv / hdmi (sonos, bluesound)
1092 "tv",
1093 "tv input",
1094 "hdmi arc",
1095 # local/usb playback on device (musiccast)
1096 "usb",
1097 # external (hass_players)
1098 "external",
1099}
1100
1101COLLECTION_ITEM_ID_SEPARATOR = "___"
1102