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