music-assistant-server

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