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