Config handling improvements (#3021)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 26 Jan 2026 19:23:03 +0000 (20:23 +0100)
committerGitHub <noreply@github.com>
Mon, 26 Jan 2026 19:23:03 +0000 (20:23 +0100)
31 files changed:
music_assistant/constants.py
music_assistant/controllers/config.py
music_assistant/controllers/players/player_controller.py
music_assistant/controllers/players/sync_groups.py
music_assistant/models/core_controller.py
music_assistant/models/player.py
music_assistant/models/provider.py
music_assistant/providers/_demo_player_provider/player.py
music_assistant/providers/airplay/player.py
music_assistant/providers/alexa/__init__.py
music_assistant/providers/ard_audiothek/__init__.py
music_assistant/providers/bluesound/player.py
music_assistant/providers/builtin/__init__.py
music_assistant/providers/chromecast/player.py
music_assistant/providers/dlna/constants.py
music_assistant/providers/dlna/player.py
music_assistant/providers/fully_kiosk/player.py
music_assistant/providers/gpodder/__init__.py
music_assistant/providers/hass_players/player.py
music_assistant/providers/heos/player.py
music_assistant/providers/musiccast/constants.py
music_assistant/providers/podcast_index/provider.py
music_assistant/providers/radiobrowser/__init__.py
music_assistant/providers/sendspin/player.py
music_assistant/providers/snapcast/player.py
music_assistant/providers/sonos/player.py
music_assistant/providers/sonos_s1/player.py
music_assistant/providers/spotify/provider.py
music_assistant/providers/squeezelite/player.py
music_assistant/providers/tidal/provider.py
music_assistant/providers/universal_group/player.py

index d9a5515a7130474590ca6f6b942ba9197614f5f6..115c23c5d3ea5d25d08cbd891e6cec4df9939714 100644 (file)
@@ -8,7 +8,7 @@ from music_assistant_models.config_entries import (
     ConfigEntry,
     ConfigValueOption,
 )
-from music_assistant_models.enums import ConfigEntryType, ContentType, HidePlayerOption
+from music_assistant_models.enums import ConfigEntryType, ContentType
 from music_assistant_models.media_items import AudioFormat
 
 APPLICATION_NAME: Final = "Music Assistant"
@@ -55,9 +55,6 @@ CONF_PASSWORD: Final[str] = "password"
 CONF_VOLUME_NORMALIZATION: Final[str] = "volume_normalization"
 CONF_VOLUME_NORMALIZATION_TARGET: Final[str] = "volume_normalization_target"
 CONF_OUTPUT_LIMITER: Final[str] = "output_limiter"
-CONF_DEPRECATED_EQ_BASS: Final[str] = "eq_bass"
-CONF_DEPRECATED_EQ_MID: Final[str] = "eq_mid"
-CONF_DEPRECATED_EQ_TREBLE: Final[str] = "eq_treble"
 CONF_PLAYER_DSP: Final[str] = "player_dsp"
 CONF_PLAYER_DSP_PRESETS: Final[str] = "player_dsp_presets"
 CONF_OUTPUT_CHANNELS: Final[str] = "output_channels"
@@ -69,10 +66,9 @@ CONF_BIND_IP: Final[str] = "bind_ip"
 CONF_BIND_PORT: Final[str] = "bind_port"
 CONF_PUBLISH_IP: Final[str] = "publish_ip"
 CONF_AUTO_PLAY: Final[str] = "auto_play"
-CONF_DEPRECATED_CROSSFADE: Final[str] = "crossfade"
 CONF_GROUP_MEMBERS: Final[str] = "group_members"
 CONF_DYNAMIC_GROUP_MEMBERS: Final[str] = "dynamic_members"
-CONF_HIDE_PLAYER_IN_UI: Final[str] = "hide_player_in_ui"
+CONF_HIDE_IN_UI: Final[str] = "hide_in_ui"
 CONF_EXPOSE_PLAYER_TO_HA: Final[str] = "expose_player_to_ha"
 CONF_SYNC_ADJUST: Final[str] = "sync_adjust"
 CONF_TTS_PRE_ANNOUNCE: Final[str] = "tts_pre_announce"
@@ -173,30 +169,9 @@ DEFAULT_CORE_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,)
 CONF_ENTRY_FLOW_MODE = ConfigEntry(
     key=CONF_FLOW_MODE,
     type=ConfigEntryType.BOOLEAN,
-    label="Enable queue flow mode",
+    label="Enforce Gapless playback with Queue Flow Mode streaming",
     default_value=False,
-)
-
-CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED = ConfigEntry.from_dict(
-    {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True}
-)
-
-CONF_ENTRY_FLOW_MODE_ENFORCED = ConfigEntry.from_dict(
-    {
-        **CONF_ENTRY_FLOW_MODE.to_dict(),
-        "default_value": True,
-        "value": True,
-        "hidden": True,
-    }
-)
-
-CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED = ConfigEntry.from_dict(
-    {
-        **CONF_ENTRY_FLOW_MODE.to_dict(),
-        "default_value": False,
-        "value": False,
-        "hidden": True,
-    }
+    category="advanced",
 )
 
 
@@ -224,6 +199,7 @@ CONF_ENTRY_OUTPUT_CHANNELS = ConfigEntry(
     default_value="stereo",
     label="Output Channel Mode",
     category="audio",
+    requires_reload=True,
 )
 
 CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry(
@@ -233,6 +209,7 @@ CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry(
     default_value=True,
     description="Enable volume normalization (EBU-R128 based)",
     category="audio",
+    requires_reload=True,
 )
 
 CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry(
@@ -244,6 +221,7 @@ CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry(
     description="Adjust average (perceived) loudness to this target level",
     depends_on=CONF_VOLUME_NORMALIZATION,
     category="advanced",
+    requires_reload=True,
 )
 
 CONF_ENTRY_OUTPUT_LIMITER = ConfigEntry(
@@ -253,64 +231,9 @@ CONF_ENTRY_OUTPUT_LIMITER = ConfigEntry(
     default_value=True,
     description="Activates a limiter that prevents audio distortion by making loud peaks quieter.",
     category="audio",
+    requires_reload=True,
 )
 
-# These EQ Options are deprecated and will be removed in the future
-# To allow for automatic migration to the new DSP system, they are still included in the config
-CONF_ENTRY_DEPRECATED_EQ_BASS = ConfigEntry(
-    key=CONF_DEPRECATED_EQ_BASS,
-    type=ConfigEntryType.INTEGER,
-    range=(-10, 10),
-    default_value=0,
-    label="Equalizer: bass",
-    description="Use the builtin basic equalizer to adjust the bass of audio.",
-    category="audio",
-    hidden=True,  # Hidden, use DSP instead
-)
-
-CONF_ENTRY_DEPRECATED_EQ_MID = ConfigEntry(
-    key=CONF_DEPRECATED_EQ_MID,
-    type=ConfigEntryType.INTEGER,
-    range=(-10, 10),
-    default_value=0,
-    label="Equalizer: midrange",
-    description="Use the builtin basic equalizer to adjust the midrange of audio.",
-    category="audio",
-    hidden=True,  # Hidden, use DSP instead
-)
-
-CONF_ENTRY_DEPRECATED_EQ_TREBLE = ConfigEntry(
-    key=CONF_DEPRECATED_EQ_TREBLE,
-    type=ConfigEntryType.INTEGER,
-    range=(-10, 10),
-    default_value=0,
-    label="Equalizer: treble",
-    description="Use the builtin basic equalizer to adjust the treble of audio.",
-    category="audio",
-    hidden=True,  # Hidden, use DSP instead
-)
-
-
-CONF_ENTRY_DEPRECATED_CROSSFADE = ConfigEntry(
-    key=CONF_DEPRECATED_CROSSFADE,
-    type=ConfigEntryType.BOOLEAN,
-    label="Enable crossfade",
-    default_value=False,
-    description="Enable a crossfade transition between (queue) tracks.",
-    category="audio",
-    hidden=True,  # Hidden, use Smart Fades instead
-)
-
-CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED = ConfigEntry(
-    key=CONF_DEPRECATED_CROSSFADE,
-    type=ConfigEntryType.BOOLEAN,
-    label="Enable crossfade",
-    default_value=False,
-    description="Enable a crossfade transition between (queue) tracks.\n\n "
-    "Requires flow-mode to be enabled",
-    category="audio",
-    depends_on=CONF_FLOW_MODE,
-)
 
 CONF_ENTRY_SMART_FADES_MODE = ConfigEntry(
     key=CONF_SMART_FADES_MODE,
@@ -328,6 +251,7 @@ CONF_ENTRY_SMART_FADES_MODE = ConfigEntry(
     "- 'Standard Crossfade': Regular crossfade that crossfades the last/first x-seconds of a "
     "track.",
     category="audio",
+    requires_reload=True,
 )
 
 CONF_ENTRY_CROSSFADE_DURATION = ConfigEntry(
@@ -341,55 +265,7 @@ CONF_ENTRY_CROSSFADE_DURATION = ConfigEntry(
     depends_on=CONF_SMART_FADES_MODE,
     depends_on_value="standard_crossfade",
     category="audio",
-)
-
-CONF_ENTRY_HIDE_PLAYER_IN_UI = ConfigEntry(
-    key=CONF_HIDE_PLAYER_IN_UI,
-    type=ConfigEntryType.STRING,
-    label="Hide this player in the user interface",
-    multi_value=True,
-    options=[
-        ConfigValueOption("Always", HidePlayerOption.ALWAYS.value),
-        ConfigValueOption("When powered off", HidePlayerOption.WHEN_OFF.value),
-        ConfigValueOption("When group active", HidePlayerOption.WHEN_GROUP_ACTIVE.value),
-        ConfigValueOption("When synced", HidePlayerOption.WHEN_SYNCED.value),
-        ConfigValueOption("When unavailable", HidePlayerOption.WHEN_UNAVAILABLE.value),
-    ],
-    default_value=[
-        HidePlayerOption.WHEN_UNAVAILABLE.value,
-        HidePlayerOption.WHEN_GROUP_ACTIVE.value,
-        HidePlayerOption.WHEN_SYNCED.value,
-    ],
-)
-CONF_ENTRY_HIDE_PLAYER_IN_UI_ALWAYS_DEFAULT = ConfigEntry.from_dict(
-    {**CONF_ENTRY_HIDE_PLAYER_IN_UI.to_dict(), "default_value": [HidePlayerOption.ALWAYS.value]}
-)
-
-CONF_ENTRY_HIDE_PLAYER_IN_UI_GROUP_PLAYER = ConfigEntry.from_dict(
-    {
-        **CONF_ENTRY_HIDE_PLAYER_IN_UI.to_dict(),
-        "default_value": [HidePlayerOption.WHEN_UNAVAILABLE.value],
-        "options": [
-            ConfigValueOption("Always", HidePlayerOption.ALWAYS.value).to_dict(),
-            ConfigValueOption("When powered off", HidePlayerOption.WHEN_OFF.value).to_dict(),
-            ConfigValueOption(
-                "When unavailable", HidePlayerOption.WHEN_UNAVAILABLE.value
-            ).to_dict(),
-        ],
-    }
-)
-
-CONF_ENTRY_EXPOSE_PLAYER_TO_HA = ConfigEntry(
-    key=CONF_EXPOSE_PLAYER_TO_HA,
-    type=ConfigEntryType.BOOLEAN,
-    label="Expose this player to Home Assistant",
-    default_value=True,
-    description="Expose this player to the Home Assistant integration. \n"
-    "If disabled, this player will not be imported into Home Assistant.",
-    category="advanced",
-)
-CONF_ENTRY_EXPOSE_PLAYER_TO_HA_DEFAULT_DISABLED = ConfigEntry.from_dict(
-    {**CONF_ENTRY_EXPOSE_PLAYER_TO_HA.to_dict(), "default_value": False}
+    requires_reload=True,
 )
 
 
@@ -411,6 +287,7 @@ CONF_ENTRY_OUTPUT_CODEC = ConfigEntry(
     "into e.g. a lossy mp3 codec or you like to save some network bandwidth. \n\n "
     "Choosing a lossy codec saves some bandwidth at the cost of audio quality.",
     category="advanced",
+    requires_reload=True,
 )
 
 CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3 = ConfigEntry.from_dict(
@@ -447,6 +324,7 @@ CONF_ENTRY_SYNC_ADJUST = ConfigEntry(
     "and you always hear the audio too early or late on this player, "
     "you can shift the audio a bit.",
     category="advanced",
+    requires_reload=True,
 )
 
 
@@ -520,20 +398,6 @@ HIDDEN_ANNOUNCE_VOLUME_CONFIG_ENTRIES = (
     CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY_HIDDEN,
 )
 
-CONF_ENTRY_PLAYER_ICON = ConfigEntry(
-    key=CONF_ICON,
-    type=ConfigEntryType.ICON,
-    default_value="mdi-speaker",
-    label="Icon",
-    description="Material design icon for this player. "
-    "\n\nSee https://pictogrammers.com/library/mdi/",
-    category="generic",
-)
-
-CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict(
-    {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "mdi-speaker-multiple"}
-)
-
 
 CONF_ENTRY_SAMPLE_RATES = ConfigEntry(
     key=CONF_SAMPLE_RATES,
@@ -563,6 +427,7 @@ CONF_ENTRY_SAMPLE_RATES = ConfigEntry(
     category="advanced",
     description="The sample rates (and bit depths) supported by this player.\n"
     "Content with unsupported sample rates will be automatically resampled.",
+    requires_reload=True,
 )
 
 
@@ -580,6 +445,7 @@ CONF_ENTRY_HTTP_PROFILE = ConfigEntry(
     description="This is considered to be a very advanced setting, only adjust this if needed, "
     "for example if your player stops playing halfway streams or if you experience "
     "other playback related issues. In most cases the default setting is fine.",
+    requires_reload=True,
 )
 
 CONF_ENTRY_HTTP_PROFILE_DEFAULT_1 = ConfigEntry.from_dict(
@@ -617,6 +483,7 @@ CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry(
         ConfigValueOption("Profile 2 - full info (including image)", "full"),
     ],
     depends_on=CONF_FLOW_MODE,
+    depends_on_value_not=False,
     default_value="disabled",
     label="Try to inject metadata into stream (ICY)",
     category="advanced",
@@ -624,6 +491,7 @@ CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry(
     "even when flow mode is enabled.\n\nThis is called ICY metadata and is what is used by "
     "online radio stations to show you what is playing. \n\nBe aware that not all players support "
     "this correctly. If you experience issues with playback, try disabling this setting.",
+    requires_reload=True,
 )
 
 CONF_ENTRY_ENABLE_ICY_METADATA_HIDDEN = ConfigEntry.from_dict(
@@ -633,12 +501,19 @@ CONF_ENTRY_ENABLE_ICY_METADATA_HIDDEN = ConfigEntry.from_dict(
 CONF_ENTRY_ICY_METADATA_HIDDEN_DISABLED = ConfigEntry.from_dict(
     {
         **CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(),
-        "default_value": False,
-        "value": False,
+        "default_value": "disabled",
+        "value": "disabled",
         "hidden": True,
     }
 )
 
+CONF_ENTRY_ICY_METADATA_DEFAULT_FULL = ConfigEntry.from_dict(
+    {
+        **CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(),
+        "default_value": "full",
+    }
+)
+
 CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES = ConfigEntry(
     key="gapless_different_sample_rates",
     type=ConfigEntryType.BOOLEAN,
@@ -649,6 +524,7 @@ CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES = ConfigEntry(
     "experience audio glitches during transitioning between tracks.",
     default_value=False,
     category="advanced",
+    requires_reload=True,
 )
 
 CONF_ENTRY_WARN_PREVIEW = ConfigEntry(
@@ -909,6 +785,21 @@ CONF_ENTRY_PROVIDER_SYNC_INTERVAL_RADIOS = ConfigEntry(
 )
 
 
+CONF_ENTRY_PLAYER_ICON = ConfigEntry(
+    key=CONF_ICON,
+    type=ConfigEntryType.ICON,
+    default_value="mdi-speaker",
+    label="Icon",
+    description="Material design icon for this player. "
+    "\n\nSee https://pictogrammers.com/library/mdi/",
+    category="generic",
+)
+
+CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict(
+    {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "mdi-speaker-multiple"}
+)
+
+
 def create_sample_rates_config_entry(
     supported_sample_rates: list[int] | None = None,
     supported_bit_depths: list[int] | None = None,
@@ -1009,3 +900,9 @@ SOUNDTRACK_INDICATORS = [
     r"\bfrom the film\b",
     r"\boriginal.*cast.*recording\b",
 ]
+
+# List of providers that do not use HTTP streaming
+# but consume raw audio data over other protocols
+# for provider domains in this list, we won't show the default
+# http-streaming specific config options in player settings
+NON_HTTP_PROVIDERS = ("airplay", "sendspin", "snapcast")
index 9a07583c56e4e9235fdbd3030f14bb67f9446a1d..3f016d5923d3f45efcab239fc7fc92452343bd87 100644 (file)
@@ -18,13 +18,26 @@ from music_assistant_models import config_entries
 from music_assistant_models.config_entries import (
     MULTI_VALUE_SPLITTER,
     ConfigEntry,
+    ConfigValueOption,
     ConfigValueType,
     CoreConfig,
     PlayerConfig,
     ProviderConfig,
 )
-from music_assistant_models.dsp import DSPConfig, DSPConfigPreset, ToneControlFilter
-from music_assistant_models.enums import EventType, ProviderFeature, ProviderType
+from music_assistant_models.constants import (
+    PLAYER_CONTROL_FAKE,
+    PLAYER_CONTROL_NATIVE,
+    PLAYER_CONTROL_NONE,
+)
+from music_assistant_models.dsp import DSPConfig, DSPConfigPreset
+from music_assistant_models.enums import (
+    ConfigEntryType,
+    EventType,
+    PlayerFeature,
+    PlayerType,
+    ProviderFeature,
+    ProviderType,
+)
 from music_assistant_models.errors import (
     ActionUnavailable,
     InvalidDataError,
@@ -34,10 +47,15 @@ from music_assistant_models.helpers import get_global_cache_value
 
 from music_assistant.constants import (
     CONF_CORE,
-    CONF_DEPRECATED_CROSSFADE,
-    CONF_DEPRECATED_EQ_BASS,
-    CONF_DEPRECATED_EQ_MID,
-    CONF_DEPRECATED_EQ_TREBLE,
+    CONF_ENTRY_ANNOUNCE_VOLUME,
+    CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
+    CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
+    CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
+    CONF_ENTRY_AUTO_PLAY,
+    CONF_ENTRY_CROSSFADE_DURATION,
+    CONF_ENTRY_ENABLE_ICY_METADATA,
+    CONF_ENTRY_FLOW_MODE,
+    CONF_ENTRY_HTTP_PROFILE,
     CONF_ENTRY_LIBRARY_SYNC_ALBUM_TRACKS,
     CONF_ENTRY_LIBRARY_SYNC_ALBUMS,
     CONF_ENTRY_LIBRARY_SYNC_ARTISTS,
@@ -48,6 +66,11 @@ from music_assistant.constants import (
     CONF_ENTRY_LIBRARY_SYNC_PODCASTS,
     CONF_ENTRY_LIBRARY_SYNC_RADIOS,
     CONF_ENTRY_LIBRARY_SYNC_TRACKS,
+    CONF_ENTRY_OUTPUT_CHANNELS,
+    CONF_ENTRY_OUTPUT_CODEC,
+    CONF_ENTRY_OUTPUT_LIMITER,
+    CONF_ENTRY_PLAYER_ICON,
+    CONF_ENTRY_PLAYER_ICON_GROUP,
     CONF_ENTRY_PROVIDER_SYNC_INTERVAL_ALBUMS,
     CONF_ENTRY_PROVIDER_SYNC_INTERVAL_ARTISTS,
     CONF_ENTRY_PROVIDER_SYNC_INTERVAL_AUDIOBOOKS,
@@ -55,27 +78,41 @@ from music_assistant.constants import (
     CONF_ENTRY_PROVIDER_SYNC_INTERVAL_PODCASTS,
     CONF_ENTRY_PROVIDER_SYNC_INTERVAL_RADIOS,
     CONF_ENTRY_PROVIDER_SYNC_INTERVAL_TRACKS,
+    CONF_ENTRY_SAMPLE_RATES,
+    CONF_ENTRY_SMART_FADES_MODE,
+    CONF_ENTRY_TTS_PRE_ANNOUNCE,
+    CONF_ENTRY_VOLUME_NORMALIZATION,
+    CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
+    CONF_EXPOSE_PLAYER_TO_HA,
+    CONF_HIDE_IN_UI,
+    CONF_MUTE_CONTROL,
     CONF_ONBOARD_DONE,
     CONF_PLAYER_DSP,
     CONF_PLAYER_DSP_PRESETS,
     CONF_PLAYERS,
+    CONF_POWER_CONTROL,
+    CONF_PRE_ANNOUNCE_CHIME_URL,
     CONF_PROVIDERS,
     CONF_SERVER_ID,
     CONF_SMART_FADES_MODE,
+    CONF_VOLUME_CONTROL,
     CONFIGURABLE_CORE_CONTROLLERS,
     DEFAULT_CORE_CONFIG_ENTRIES,
     DEFAULT_PROVIDER_CONFIG_ENTRIES,
     ENCRYPT_SUFFIX,
+    NON_HTTP_PROVIDERS,
 )
+from music_assistant.controllers.players.sync_groups import SyncGroupPlayer
 from music_assistant.helpers.api import api_command
 from music_assistant.helpers.json import JSON_DECODE_EXCEPTIONS, async_json_dumps, async_json_loads
-from music_assistant.helpers.util import load_provider_module
+from music_assistant.helpers.util import load_provider_module, validate_announcement_chime_url
 from music_assistant.models import ProviderModuleType
 from music_assistant.models.music_provider import MusicProvider
 
 if TYPE_CHECKING:
     from music_assistant import MusicAssistant
     from music_assistant.models.core_controller import CoreController
+    from music_assistant.models.player import Player
 
 LOGGER = logging.getLogger(__name__)
 DEFAULT_SAVE_DELAY = 5
@@ -496,19 +533,26 @@ class ConfigController:
         self, provider: str | None = None, include_values: bool = False
     ) -> list[PlayerConfig]:
         """Return all known player configurations, optionally filtered by provider id."""
-        return [
-            await self.get_player_config(raw_conf["player_id"])
-            if include_values
-            else cast("PlayerConfig", PlayerConfig.parse([], raw_conf))
-            for raw_conf in list(self.get(CONF_PLAYERS, {}).values())
-            # filter out unavailable providers (only if we requested the full info)
-            if (
-                not include_values
-                or raw_conf["provider"] in get_global_cache_value("available_providers", [])
-            )
+        result: list[PlayerConfig] = []
+        for raw_conf in list(self.get(CONF_PLAYERS, {}).values()):
+            # filter out unavailable providers
+            if raw_conf["provider"] not in get_global_cache_value("available_providers", []):
+                continue
             # optional provider filter
-            and (provider in (None, raw_conf["provider"]))
-        ]
+            if provider is not None and raw_conf["provider"] != provider:
+                continue
+            # filter out unavailable players
+            # (unless disabled, otherwise there is no way to re-enable them)
+            player = self.mass.players.get(raw_conf["player_id"], False)
+            if (not player or not player.available) and raw_conf.get("enabled", True):
+                continue
+
+            if include_values:
+                result.append(await self.get_player_config(raw_conf["player_id"]))
+            else:
+                raw_conf["default_name"] = player.display_name if player else raw_conf.get("name")
+                result.append(cast("PlayerConfig", PlayerConfig.parse([], raw_conf)))
+        return result
 
     @api_command("config/players/get")
     async def get_player_config(
@@ -526,12 +570,13 @@ class ConfigController:
                 # pass action and values to get_config_entries
                 if values is None:
                     values = raw_conf.get("values", {})
-                conf_entries = await player.get_config_entries(action=action, values=values)
+                conf_entries = await self.get_player_config_entries(
+                    player_id, action=action, values=values
+                )
             else:
                 # handle unavailable player and/or provider
                 conf_entries = []
                 raw_conf["available"] = False
-                raw_conf["name"] = raw_conf.get("name")
                 raw_conf["default_name"] = raw_conf.get("default_name") or raw_conf["player_id"]
             return cast("PlayerConfig", PlayerConfig.parse(conf_entries, raw_conf))
         msg = f"No config found for player id {player_id}"
@@ -558,7 +603,14 @@ class ConfigController:
         if values is None:
             values = self.get(f"{CONF_PLAYERS}/{player_id}/values", {})
 
-        all_entries = await player.get_config_entries(action=action, values=values)
+        player_entries = await player.get_config_entries(action=action, values=values)
+        default_entries = self._get_default_player_config_entries(player)
+        player_entries_keys = {entry.key for entry in player_entries}
+        all_entries = [
+            *player_entries,
+            # ignore default entries that were overridden by the player specific ones
+            *[x for x in default_entries if x.key not in player_entries_keys],
+        ]
         # set current value from stored values
         for entry in all_entries:
             if entry.value is None:
@@ -752,51 +804,8 @@ class ConfigController:
             return DSPConfig.from_dict(raw_conf)
         # return default DSP config
         dsp_config = DSPConfig()
-
-        deprecated_eq_bass = self.mass.config.get_raw_player_config_value(
-            player_id, CONF_DEPRECATED_EQ_BASS, 0
-        )
-        deprecated_eq_mid = self.mass.config.get_raw_player_config_value(
-            player_id, CONF_DEPRECATED_EQ_MID, 0
-        )
-        deprecated_eq_treble = self.mass.config.get_raw_player_config_value(
-            player_id, CONF_DEPRECATED_EQ_TREBLE, 0
-        )
-        if deprecated_eq_bass != 0 or deprecated_eq_mid != 0 or deprecated_eq_treble != 0:
-            # the user previously used the now deprecated EQ settings:
-            # add a tone control filter with the old values, reset the deprecated values and
-            # save this as the new DSP config
-            # TODO: remove this in a future release
-            dsp_config.enabled = True
-            dsp_config.filters.append(
-                ToneControlFilter(
-                    enabled=True,
-                    bass_level=float(deprecated_eq_bass)
-                    if isinstance(deprecated_eq_bass, (int, float, str))
-                    else 0.0,
-                    mid_level=float(deprecated_eq_mid)
-                    if isinstance(deprecated_eq_mid, (int, float, str))
-                    else 0.0,
-                    treble_level=float(deprecated_eq_treble)
-                    if isinstance(deprecated_eq_treble, (int, float, str))
-                    else 0.0,
-                )
-            )
-
-            deprecated_eq_keys = [
-                CONF_DEPRECATED_EQ_BASS,
-                CONF_DEPRECATED_EQ_MID,
-                CONF_DEPRECATED_EQ_TREBLE,
-            ]
-            for key in deprecated_eq_keys:
-                if self.mass.config.get_raw_player_config_value(player_id, key, 0) != 0:
-                    self.mass.config.set_raw_player_config_value(player_id, key, 0)
-
-            self.set(f"{CONF_PLAYER_DSP}/{player_id}", dsp_config.to_dict())
-        else:
-            # The DSP config does not do anything by default, so we disable it
-            dsp_config.enabled = False
-
+        # The DSP config does not do anything by default, so we disable it
+        dsp_config.enabled = False
         return dsp_config
 
     @api_command("config/players/dsp/save", required_role="admin")
@@ -1076,7 +1085,7 @@ class ConfigController:
         self.save(immediate=True)
         try:
             controller: CoreController = getattr(self.mass, domain)
-            await controller.reload(config)
+            await controller.update_config(config, changed_keys)
         except asyncio.CancelledError:
             pass
         except Exception:
@@ -1336,18 +1345,6 @@ class ConfigController:
                 self._data[CONF_PROVIDERS]["sendspin"] = provider_config
                 changed = True
 
-        # Migrate the crossfade setting into Smart Fade Mode = 'crossfade'
-        for player_config in self._data.get(CONF_PLAYERS, {}).values():
-            if not (values := player_config.get("values")):
-                continue
-            if (crossfade := values.pop(CONF_DEPRECATED_CROSSFADE, None)) is None:
-                continue
-            # Check if player has old crossfade enabled but no smart fades mode set
-            if crossfade is True and CONF_SMART_FADES_MODE not in values:
-                # Set smart fades mode to standard_crossfade
-                values[CONF_SMART_FADES_MODE] = "standard_crossfade"
-                changed = True
-
         # Migrate smart_fades mode value to smart_crossfade
         for player_config in self._data.get(CONF_PLAYERS, {}).values():
             if not (values := player_config.get("values")):
@@ -1433,7 +1430,8 @@ class ConfigController:
         """Update ProviderConfig."""
         config = await self.get_provider_config(instance_id)
         changed_keys = config.update(values)
-        available = prov.available if (prov := self.mass.get_provider(instance_id)) else False
+        prov_instance = self.mass.get_provider(instance_id)
+        available = prov_instance.available if prov_instance else False
         if not changed_keys and (config.enabled == available):
             # no changes
             return config
@@ -1444,7 +1442,13 @@ class ConfigController:
         conf_key = f"{CONF_PROVIDERS}/{config.instance_id}"
         raw_conf = config.to_raw()
         self.set(conf_key, raw_conf)
-        if config.enabled:
+        if config.enabled and prov_instance is None:
+            await self.mass.load_provider_config(config)
+        if config.enabled and prov_instance and available:
+            # update config for existing/loaded provider instance
+            await prov_instance.update_config(config, changed_keys)
+        elif config.enabled:
+            # provider is enabled but not available, try to load it
             await self.mass.load_provider_config(config)
         else:
             # disable provider
@@ -1534,3 +1538,194 @@ class ConfigController:
             # correct any multi-instance provider mappings
             self.mass.create_task(self.mass.music.correct_multi_instance_provider_mappings())
         return config
+
+    def _get_default_player_config_entries(self, player: Player) -> list[ConfigEntry]:
+        """Return the default player config entries."""
+        entries: list[ConfigEntry] = []
+        # default protocol-player config entries
+        if player.type == PlayerType.PROTOCOL:
+            # bare minimum: only playback related entries
+            entries += [
+                CONF_ENTRY_OUTPUT_CHANNELS,
+            ]
+            if not player.requires_flow_mode:
+                entries.append(CONF_ENTRY_FLOW_MODE)
+            if player.provider.domain not in NON_HTTP_PROVIDERS:
+                entries += [
+                    CONF_ENTRY_SAMPLE_RATES,
+                    CONF_ENTRY_OUTPUT_CODEC,
+                    CONF_ENTRY_HTTP_PROFILE,
+                    CONF_ENTRY_ENABLE_ICY_METADATA,
+                ]
+            return entries
+
+        # some base entries for all player types
+        entries += [
+            CONF_ENTRY_SMART_FADES_MODE,
+            CONF_ENTRY_CROSSFADE_DURATION,
+            CONF_ENTRY_VOLUME_NORMALIZATION,
+            CONF_ENTRY_OUTPUT_LIMITER,
+            CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
+            CONF_ENTRY_TTS_PRE_ANNOUNCE,
+            ConfigEntry(
+                key=CONF_PRE_ANNOUNCE_CHIME_URL,
+                type=ConfigEntryType.STRING,
+                label="Custom (pre)announcement chime URL",
+                description="URL to a custom audio file to play before announcements.\n"
+                "Leave empty to use the default chime.\n"
+                "Supports http:// and https:// URLs pointing to "
+                "audio files (.mp3, .wav, .flac, .ogg, .m4a, .aac).\n"
+                "Example: http://homeassistant.local:8123/local/audio/custom_chime.mp3",
+                category="announcements",
+                required=False,
+                depends_on=CONF_ENTRY_TTS_PRE_ANNOUNCE.key,
+                depends_on_value=True,
+                validate=lambda val: validate_announcement_chime_url(cast("str", val)),
+            ),
+            # add player control entries
+            *self._create_player_control_config_entries(player),
+            # add entry to hide player in UI
+            ConfigEntry(
+                key=CONF_HIDE_IN_UI,
+                type=ConfigEntryType.BOOLEAN,
+                label="Hide this player in the user interface",
+                default_value=player.hidden_by_default,
+                category="advanced",
+            ),
+            # add entry to expose player to HA
+            ConfigEntry(
+                key=CONF_EXPOSE_PLAYER_TO_HA,
+                type=ConfigEntryType.BOOLEAN,
+                label="Expose this player to Home Assistant",
+                description="Expose this player to the Home Assistant integration. \n"
+                "If disabled, this player will not be imported into Home Assistant.",
+                category="advanced",
+                default_value=player.expose_to_ha_by_default,
+            ),
+        ]
+
+        # group-player config entries
+        if player.type == PlayerType.GROUP:
+            is_dedicated_group_player = (
+                not isinstance(player, SyncGroupPlayer)
+                and player.provider.domain != "universal_group"
+            )
+            entries += [
+                CONF_ENTRY_PLAYER_ICON_GROUP,
+            ]
+            if is_dedicated_group_player and not player.requires_flow_mode:
+                entries.append(CONF_ENTRY_FLOW_MODE)
+            if is_dedicated_group_player and player.provider.domain not in NON_HTTP_PROVIDERS:
+                entries += [
+                    CONF_ENTRY_SAMPLE_RATES,
+                    CONF_ENTRY_OUTPUT_CODEC,
+                    CONF_ENTRY_HTTP_PROFILE,
+                    CONF_ENTRY_ENABLE_ICY_METADATA,
+                ]
+            return entries
+
+        # normal player (or stereo pair) config entries
+        entries += [
+            CONF_ENTRY_PLAYER_ICON,
+            CONF_ENTRY_OUTPUT_CHANNELS,
+            # add default entries for announce feature
+            CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
+            CONF_ENTRY_ANNOUNCE_VOLUME,
+            CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
+            CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
+        ]
+        # add flow mode config entry for players that not already explicitly enable it
+        if not player.requires_flow_mode:
+            entries.append(CONF_ENTRY_FLOW_MODE)
+        # add HTTP streaming config entries for non-http players
+        if player.provider.domain not in NON_HTTP_PROVIDERS:
+            entries += [
+                CONF_ENTRY_SAMPLE_RATES,
+                CONF_ENTRY_OUTPUT_CODEC,
+                CONF_ENTRY_HTTP_PROFILE,
+                CONF_ENTRY_ENABLE_ICY_METADATA,
+            ]
+
+        return entries
+
+    def _create_player_control_config_entries(self, player: Player) -> list[ConfigEntry]:
+        """Create config entries for player controls."""
+        all_controls = self.mass.players.player_controls()
+        power_controls = [x for x in all_controls if x.supports_power]
+        volume_controls = [x for x in all_controls if x.supports_volume]
+        mute_controls = [x for x in all_controls if x.supports_mute]
+        # work out player supported features
+        supports_power = PlayerFeature.POWER in player.supported_features
+        supports_volume = PlayerFeature.VOLUME_SET in player.supported_features
+        supports_mute = PlayerFeature.VOLUME_MUTE in player.supported_features
+        # create base options per control type (and add defaults like native and fake)
+        base_power_options: list[ConfigValueOption] = [
+            ConfigValueOption(title="None", value=PLAYER_CONTROL_NONE),
+            ConfigValueOption(title="Fake power control", value=PLAYER_CONTROL_FAKE),
+        ]
+        if supports_power:
+            base_power_options.append(
+                ConfigValueOption(title="Native power control", value=PLAYER_CONTROL_NATIVE),
+            )
+        base_volume_options: list[ConfigValueOption] = [
+            ConfigValueOption(title="None", value=PLAYER_CONTROL_NONE),
+        ]
+        if supports_volume:
+            base_volume_options.append(
+                ConfigValueOption(title="Native volume control", value=PLAYER_CONTROL_NATIVE),
+            )
+        base_mute_options: list[ConfigValueOption] = [
+            ConfigValueOption(title="None", value=PLAYER_CONTROL_NONE),
+            ConfigValueOption(title="Fake mute control", value=PLAYER_CONTROL_FAKE),
+        ]
+        if supports_mute:
+            base_mute_options.append(
+                ConfigValueOption(title="Native mute control", value=PLAYER_CONTROL_NATIVE),
+            )
+        # return final config entries for all options
+        return [
+            # Power control config entry
+            ConfigEntry(
+                key=CONF_POWER_CONTROL,
+                type=ConfigEntryType.STRING,
+                label="Power Control",
+                default_value=PLAYER_CONTROL_NATIVE if supports_power else PLAYER_CONTROL_NONE,
+                required=True,
+                options=[
+                    *base_power_options,
+                    *(ConfigValueOption(x.name, x.id) for x in power_controls),
+                ],
+                category="player_controls",
+                hidden=player.type == PlayerType.GROUP,
+            ),
+            # Volume control config entry
+            ConfigEntry(
+                key=CONF_VOLUME_CONTROL,
+                type=ConfigEntryType.STRING,
+                label="Volume Control",
+                default_value=PLAYER_CONTROL_NATIVE if supports_volume else PLAYER_CONTROL_NONE,
+                required=True,
+                options=[
+                    *base_volume_options,
+                    *(ConfigValueOption(x.name, x.id) for x in volume_controls),
+                ],
+                category="player_controls",
+                hidden=player.type == PlayerType.GROUP,
+            ),
+            # Mute control config entry
+            ConfigEntry(
+                key=CONF_MUTE_CONTROL,
+                type=ConfigEntryType.STRING,
+                label="Mute Control",
+                default_value=PLAYER_CONTROL_NATIVE if supports_mute else PLAYER_CONTROL_NONE,
+                required=True,
+                options=[
+                    *base_mute_options,
+                    *[ConfigValueOption(x.name, x.id) for x in mute_controls],
+                ],
+                category="player_controls",
+                hidden=player.type == PlayerType.GROUP,
+            ),
+            # auto-play on power on control config entry
+            CONF_ENTRY_AUTO_PLAY,
+        ]
index c71daa399a207a92eb8505b9dc6f5f750077c20a..2a6b2bd5fae522c49e1244f502b239e5153cbe14 100644 (file)
@@ -1785,29 +1785,16 @@ class PlayerController(CoreController):
         await player.on_config_updated()
         player.update_state()
         # if the PlayerQueue was playing, restart playback
-        # TODO: add restart_stream property to ConfigEntry and use that instead of immediate_apply
-        # to check if we need to restart playback
         if not player_disabled and resume_queue and resume_queue.state == PlaybackState.PLAYING:
-            config_entries = await player.get_config_entries()
-            has_value_changes = False
-            all_immediate_apply = True
-            for key in changed_keys:
-                if not key.startswith("values/"):
-                    continue  # skip root values like "enabled", "name"
-                has_value_changes = True
-                actual_key = key.removeprefix("values/")
-                entry = next((e for e in config_entries if e.key == actual_key), None)
-                if entry is None or not entry.immediate_apply:
-                    all_immediate_apply = False
-                    break
-
-            if has_value_changes and all_immediate_apply:
-                # All changed config entries have immediate_apply=True, so no need to restart
-                # the playback
-                return
-            # always stop first to ensure the player uses the new config
-            await self.mass.player_queues.stop(resume_queue.queue_id)
-            self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id, False)
+            requires_restart = any(
+                v for v in config.values.values() if v.key in changed_keys and v.requires_reload
+            )
+            if requires_restart:
+                # always stop first to ensure the player uses the new config
+                await self.mass.player_queues.stop(resume_queue.queue_id)
+                self.mass.call_later(
+                    1, self.mass.player_queues.resume, resume_queue.queue_id, False
+                )
 
     async def on_player_dsp_change(self, player_id: str) -> None:
         """Call (by config manager) when the DSP settings of a player change."""
index d5540e33513652daff36cf8195a009daa071c21a..9fba2e172f256057119421850d52f20a4c3db118 100644 (file)
@@ -132,16 +132,11 @@ class SyncGroupPlayer(GroupPlayer):
             return self.sync_leader.playback_state if self.sync_leader else PlaybackState.IDLE
         return PlaybackState.IDLE
 
-    @cached_property
-    def flow_mode(self) -> bool:
-        """
-        Return if the player needs flow mode.
-
-        Will by default be set to True if the player does not support PlayerFeature.ENQUEUE
-        or has a flow mode config entry set to True.
-        """
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player needs flow mode."""
         if leader := self.sync_leader:
-            return leader.flow_mode
+            return leader.requires_flow_mode
         return False
 
     @property
@@ -192,9 +187,7 @@ class SyncGroupPlayer(GroupPlayer):
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the given player (if any)."""
         entries: list[ConfigEntry] = [
-            # default entries for player groups
-            *await super().get_config_entries(action=action, values=values),
-            # add syncgroup specific entries
+            # syncgroup specific entries
             ConfigEntry(
                 key=CONF_GROUP_MEMBERS,
                 type=ConfigEntryType.STRING,
index be91dd25ffeaef85c45c9520219aa5275b64995b..89332628e5c381d9f2477e01bc6c376512b36993 100644 (file)
@@ -61,6 +61,22 @@ class CoreController:
         self._set_logger(log_level)
         await self.setup(config)
 
+    async def update_config(self, config: CoreConfig, changed_keys: set[str]) -> None:
+        """Handle logic when the config is updated."""
+        # default implementation: perform a full reload on any config change
+        # TODO: only reload when 'requires_reload' keys changed
+        if changed_keys == {f"values/{CONF_LOG_LEVEL}"}:
+            # only log level changed, no need to reload
+            log_value = str(config.get_value(CONF_LOG_LEVEL))
+            self._set_logger(log_value)
+        else:
+            self.logger.info(
+                "Config updated, reloading %s core controller",
+                self.manifest.name,
+            )
+            task_id = f"core_reload_{self.domain}"
+            self.mass.call_later(1, self.reload, config, task_id=task_id)
+
     def _set_logger(self, log_level: str | None = None) -> None:
         """Set the logger settings."""
         mass_logger = logging.getLogger(MASS_LOGGER_NAME)
index b1adfb381bcb4deaccec46c95cd20836460d04d6..98c0b7123556e28c64336aa00bf753796bc1c627 100644 (file)
@@ -15,26 +15,13 @@ from collections.abc import Callable
 from copy import deepcopy
 from typing import TYPE_CHECKING, Any, cast, final
 
-from music_assistant_models.config_entries import (
-    ConfigEntry,
-    ConfigValueOption,
-    ConfigValueType,
-    PlayerConfig,
-)
 from music_assistant_models.constants import (
     EXTRA_ATTRIBUTES_TYPES,
     PLAYER_CONTROL_FAKE,
     PLAYER_CONTROL_NATIVE,
     PLAYER_CONTROL_NONE,
 )
-from music_assistant_models.enums import (
-    ConfigEntryType,
-    HidePlayerOption,
-    MediaType,
-    PlaybackState,
-    PlayerFeature,
-    PlayerType,
-)
+from music_assistant_models.enums import MediaType, PlaybackState, PlayerFeature, PlayerType
 from music_assistant_models.errors import UnsupportedFeaturedException
 from music_assistant_models.player import DeviceInfo, PlayerMedia, PlayerSource
 from music_assistant_models.player import Player as PlayerState
@@ -46,75 +33,21 @@ from music_assistant.constants import (
     ATTR_FAKE_MUTE,
     ATTR_FAKE_POWER,
     ATTR_FAKE_VOLUME,
-    CONF_ENTRY_ANNOUNCE_VOLUME,
-    CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
-    CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
-    CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
-    CONF_ENTRY_AUTO_PLAY,
-    CONF_ENTRY_CROSSFADE_DURATION,
-    CONF_ENTRY_EXPOSE_PLAYER_TO_HA,
-    CONF_ENTRY_EXPOSE_PLAYER_TO_HA_DEFAULT_DISABLED,
-    CONF_ENTRY_FLOW_MODE,
-    CONF_ENTRY_HIDE_PLAYER_IN_UI,
-    CONF_ENTRY_HIDE_PLAYER_IN_UI_ALWAYS_DEFAULT,
-    CONF_ENTRY_HIDE_PLAYER_IN_UI_GROUP_PLAYER,
-    CONF_ENTRY_HTTP_PROFILE,
-    CONF_ENTRY_OUTPUT_CHANNELS,
-    CONF_ENTRY_OUTPUT_CODEC,
-    CONF_ENTRY_OUTPUT_LIMITER,
     CONF_ENTRY_PLAYER_ICON,
-    CONF_ENTRY_PLAYER_ICON_GROUP,
-    CONF_ENTRY_SAMPLE_RATES,
-    CONF_ENTRY_SMART_FADES_MODE,
-    CONF_ENTRY_TTS_PRE_ANNOUNCE,
-    CONF_ENTRY_VOLUME_NORMALIZATION,
-    CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
     CONF_EXPOSE_PLAYER_TO_HA,
     CONF_FLOW_MODE,
-    CONF_HIDE_PLAYER_IN_UI,
+    CONF_HIDE_IN_UI,
     CONF_MUTE_CONTROL,
     CONF_POWER_CONTROL,
-    CONF_PRE_ANNOUNCE_CHIME_URL,
+    CONF_SMART_FADES_MODE,
     CONF_VOLUME_CONTROL,
 )
-from music_assistant.helpers.util import (
-    get_changed_dataclass_values,
-    validate_announcement_chime_url,
-)
+from music_assistant.helpers.util import get_changed_dataclass_values
 
 if TYPE_CHECKING:
-    from .player_provider import PlayerProvider
-
-
-CONF_ENTRY_PRE_ANNOUNCE_CUSTOM_CHIME_URL = ConfigEntry(
-    key=CONF_PRE_ANNOUNCE_CHIME_URL,
-    type=ConfigEntryType.STRING,
-    label="Custom (pre)announcement chime URL",
-    description="URL to a custom audio file to play before announcements.\n"
-    "Leave empty to use the default chime.\n"
-    "Supports http:// and https:// URLs pointing to "
-    "audio files (.mp3, .wav, .flac, .ogg, .m4a, .aac).\n"
-    "Example: http://homeassistant.local:8123/local/audio/custom_chime.mp3",
-    category="announcements",
-    required=False,
-    depends_on=CONF_ENTRY_TTS_PRE_ANNOUNCE.key,
-    depends_on_value=True,
-    validate=lambda val: validate_announcement_chime_url(cast("str", val)),
-)
+    from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, PlayerConfig
 
-BASE_CONFIG_ENTRIES = [
-    # config entries that are valid for all player types
-    CONF_ENTRY_PLAYER_ICON,
-    CONF_ENTRY_FLOW_MODE,
-    CONF_ENTRY_SMART_FADES_MODE,
-    CONF_ENTRY_CROSSFADE_DURATION,
-    CONF_ENTRY_VOLUME_NORMALIZATION,
-    CONF_ENTRY_OUTPUT_LIMITER,
-    CONF_ENTRY_VOLUME_NORMALIZATION_TARGET,
-    CONF_ENTRY_TTS_PRE_ANNOUNCE,
-    CONF_ENTRY_PRE_ANNOUNCE_CUSTOM_CHIME_URL,
-    CONF_ENTRY_HTTP_PROFILE,
-]
+    from .player_provider import PlayerProvider
 
 
 class Player(ABC):
@@ -211,17 +144,22 @@ class Player(ABC):
         """Return the current playback state of the player."""
         return self._attr_playback_state
 
-    @cached_property
-    def flow_mode(self) -> bool:
+    @property
+    def requires_flow_mode(self) -> bool:
         """
         Return if the player needs flow mode.
 
         Will by default be set to True if the player does not support PlayerFeature.ENQUEUE
-        or has a flow mode config entry set to True.
+        or has crossfade enabled without gapless support.
         """
-        if bool(self._config.get_value(CONF_FLOW_MODE)) is True:
+        if PlayerFeature.ENQUEUE not in self.supported_features:
+            # without enqueue support, flow mode is required
             return True
-        return PlayerFeature.ENQUEUE not in self.supported_features
+        return (
+            # player has crossfade enabled without gapless support - flow mode is required
+            PlayerFeature.GAPLESS_PLAYBACK not in self.supported_features
+            and str(self._config.get_value(CONF_SMART_FADES_MODE)) != "disabled"
+        )
 
     @property
     def device_info(self) -> DeviceInfo:
@@ -588,42 +526,16 @@ class Player(ABC):
         action: str | None = None,
         values: dict[str, ConfigValueType] | None = None,
     ) -> list[ConfigEntry]:
-        """Return all (provider/player specific) Config Entries for the player.
+        """
+        Return all (provider/player specific) Config Entries for the player.
 
         action: [optional] action key called from config entries UI.
         values: the (intermediate) raw values for config entries sent with the action.
         """
-        # Return all base config entries for a player.
-        # Feel free to override but ensure to include the base entries by calling super() first.
+        # Return any (player/provider specific) config entries for a player.
         # To override the default config entries, simply define an entry with the same key
         # and it will be used instead of the default one.
-        return [
-            # config entries that are valid for all players
-            *BASE_CONFIG_ENTRIES,
-            # add player control entries
-            *self._create_player_control_config_entries(),
-            CONF_ENTRY_AUTO_PLAY,
-            # audio-related config entries
-            CONF_ENTRY_SAMPLE_RATES,
-            CONF_ENTRY_OUTPUT_CODEC,
-            CONF_ENTRY_OUTPUT_CHANNELS,
-            # add default entries for announce feature
-            CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY,
-            CONF_ENTRY_ANNOUNCE_VOLUME,
-            CONF_ENTRY_ANNOUNCE_VOLUME_MIN,
-            CONF_ENTRY_ANNOUNCE_VOLUME_MAX,
-            # add default entries to hide player in UI and expose to HA
-            (
-                CONF_ENTRY_HIDE_PLAYER_IN_UI_ALWAYS_DEFAULT
-                if self.hidden_by_default
-                else CONF_ENTRY_HIDE_PLAYER_IN_UI
-            ),
-            (
-                CONF_ENTRY_EXPOSE_PLAYER_TO_HA
-                if self.expose_to_ha_by_default
-                else CONF_ENTRY_EXPOSE_PLAYER_TO_HA_DEFAULT_DISABLED
-            ),
-        ]
+        return []
 
     async def on_config_updated(self) -> None:
         """
@@ -979,16 +891,13 @@ class Player(ABC):
 
     @cached_property
     @final
-    def hide_player_in_ui(self) -> set[HidePlayerOption]:
+    def hide_in_ui(self) -> bool:
         """
         Return the hide player in UI options.
 
         This is a convenience property based on the config entry.
         """
-        return {
-            HidePlayerOption(x)
-            for x in cast("list[str]", self._config.get_value(CONF_HIDE_PLAYER_IN_UI, []))
-        }
+        return bool(self._config.get_value(CONF_HIDE_IN_UI, self.hidden_by_default))
 
     @cached_property
     @final
@@ -1012,6 +921,19 @@ class Player(ABC):
         """
         return bool(self.mass.players.get_active_queue(self))
 
+    @cached_property
+    @final
+    def flow_mode(self) -> bool:
+        """
+        Return if the player needs flow mode.
+
+        Will by default be set to True if the player does not support PlayerFeature.ENQUEUE
+        or has a flow mode config entry set to True.
+        """
+        if bool(self._config.get_value(CONF_FLOW_MODE)) is True:
+            return True
+        return PlayerFeature.ENQUEUE not in self.supported_features
+
     @property
     @final
     def state(self) -> PlayerState:
@@ -1127,85 +1049,6 @@ class Player(ABC):
                 f"Player {self.display_name} does not support feature {feature.name}"
             )
 
-    def _create_player_control_config_entries(
-        self,
-    ) -> list[ConfigEntry]:
-        """Create config entries for player controls."""
-        all_controls = self.mass.players.player_controls()
-        power_controls = [x for x in all_controls if x.supports_power]
-        volume_controls = [x for x in all_controls if x.supports_volume]
-        mute_controls = [x for x in all_controls if x.supports_mute]
-        # work out player supported features
-        supports_power = PlayerFeature.POWER in self.supported_features
-        supports_volume = PlayerFeature.VOLUME_SET in self.supported_features
-        supports_mute = PlayerFeature.VOLUME_MUTE in self.supported_features
-        # create base options per control type (and add defaults like native and fake)
-        base_power_options: list[ConfigValueOption] = [
-            ConfigValueOption(title="None", value=PLAYER_CONTROL_NONE),
-            ConfigValueOption(title="Fake power control", value=PLAYER_CONTROL_FAKE),
-        ]
-        if supports_power:
-            base_power_options.append(
-                ConfigValueOption(title="Native power control", value=PLAYER_CONTROL_NATIVE),
-            )
-        base_volume_options: list[ConfigValueOption] = [
-            ConfigValueOption(title="None", value=PLAYER_CONTROL_NONE),
-        ]
-        if supports_volume:
-            base_volume_options.append(
-                ConfigValueOption(title="Native volume control", value=PLAYER_CONTROL_NATIVE),
-            )
-        base_mute_options: list[ConfigValueOption] = [
-            ConfigValueOption(title="None", value=PLAYER_CONTROL_NONE),
-            ConfigValueOption(title="Fake mute control", value=PLAYER_CONTROL_FAKE),
-        ]
-        if supports_mute:
-            base_mute_options.append(
-                ConfigValueOption(title="Native mute control", value=PLAYER_CONTROL_NATIVE),
-            )
-        # return final config entries for all options
-        return [
-            # Power control config entry
-            ConfigEntry(
-                key=CONF_POWER_CONTROL,
-                type=ConfigEntryType.STRING,
-                label="Power Control",
-                default_value=PLAYER_CONTROL_NATIVE if supports_power else PLAYER_CONTROL_NONE,
-                required=True,
-                options=[
-                    *base_power_options,
-                    *(ConfigValueOption(x.name, x.id) for x in power_controls),
-                ],
-                category="player_controls",
-            ),
-            # Volume control config entry
-            ConfigEntry(
-                key=CONF_VOLUME_CONTROL,
-                type=ConfigEntryType.STRING,
-                label="Volume Control",
-                default_value=PLAYER_CONTROL_NATIVE if supports_volume else PLAYER_CONTROL_NONE,
-                required=True,
-                options=[
-                    *base_volume_options,
-                    *(ConfigValueOption(x.name, x.id) for x in volume_controls),
-                ],
-                category="player_controls",
-            ),
-            # Mute control config entry
-            ConfigEntry(
-                key=CONF_MUTE_CONTROL,
-                type=ConfigEntryType.STRING,
-                label="Mute Control",
-                default_value=PLAYER_CONTROL_NATIVE if supports_mute else PLAYER_CONTROL_NONE,
-                required=True,
-                options=[
-                    *base_mute_options,
-                    *[ConfigValueOption(x.name, x.id) for x in mute_controls],
-                ],
-                category="player_controls",
-            ),
-        ]
-
     def _get_player_media_checksum(self) -> str:
         """Return a checksum for the current media."""
         if not (media := self.current_media):
@@ -1254,7 +1097,7 @@ class Player(ABC):
             current_media=self.current_media,
             name=self.display_name,
             enabled=self.enabled,
-            hide_in_ui="always" in self.hide_player_in_ui,
+            hide_in_ui=self.hide_in_ui,
             expose_to_ha=self.expose_to_ha,
             icon=self.icon,
             group_volume=self.group_volume,
@@ -1553,61 +1396,6 @@ class GroupPlayer(Player):
         # default implementation: groups can't be synced
         return None
 
-    async def get_config_entries(
-        self,
-        action: str | None = None,
-        values: dict[str, ConfigValueType] | None = None,
-    ) -> list[ConfigEntry]:
-        """Return all (provider/player specific) Config Entries for the player.
-
-        action: [optional] action key called from config entries UI.
-        values: the (intermediate) raw values for config entries sent with the action.
-        """
-        # Return all base config entries for a group player.
-        # Feel free to override but ensure to include the base entries by calling super() first.
-        # To override the default config entries, simply define an entry with the same key
-        # and it will be used instead of the default one.
-        return [
-            *BASE_CONFIG_ENTRIES,
-            CONF_ENTRY_PLAYER_ICON_GROUP,
-            # add player control entries as hidden entries
-            ConfigEntry(
-                key=CONF_POWER_CONTROL,
-                type=ConfigEntryType.STRING,
-                label=CONF_POWER_CONTROL,
-                default_value=PLAYER_CONTROL_NATIVE,
-                hidden=True,
-            ),
-            ConfigEntry(
-                key=CONF_VOLUME_CONTROL,
-                type=ConfigEntryType.STRING,
-                label=CONF_VOLUME_CONTROL,
-                default_value=PLAYER_CONTROL_NATIVE,
-                hidden=True,
-            ),
-            ConfigEntry(
-                key=CONF_MUTE_CONTROL,
-                type=ConfigEntryType.STRING,
-                label=CONF_MUTE_CONTROL,
-                # disable mute control for group players for now
-                # TODO: work out if all child players support mute control
-                default_value=PLAYER_CONTROL_NONE,
-                hidden=True,
-            ),
-            CONF_ENTRY_AUTO_PLAY,
-            # add default entries to hide player in UI and expose to HA
-            (
-                CONF_ENTRY_HIDE_PLAYER_IN_UI_ALWAYS_DEFAULT
-                if self.hidden_by_default
-                else CONF_ENTRY_HIDE_PLAYER_IN_UI_GROUP_PLAYER
-            ),
-            (
-                CONF_ENTRY_EXPOSE_PLAYER_TO_HA
-                if self.expose_to_ha_by_default
-                else CONF_ENTRY_EXPOSE_PLAYER_TO_HA_DEFAULT_DISABLED
-            ),
-        ]
-
     async def volume_set(self, volume_level: int) -> None:
         """
         Handle VOLUME_SET command on the player.
index 97e4c940072b36e497371eacef67a6c3877d0cff..65475efaeb08bcf3060007d49f0057c007da296c 100644 (file)
@@ -34,17 +34,7 @@ class Provider:
         self.manifest = manifest
         self.config = config
         self._supported_features = supported_features or set()
-        mass_logger = logging.getLogger(MASS_LOGGER_NAME)
-        self.logger = mass_logger.getChild(self.domain)
-        log_level = str(config.get_value(CONF_LOG_LEVEL))
-        if log_level == "GLOBAL":
-            self.logger.setLevel(mass_logger.level)
-        else:
-            self.logger.setLevel(log_level)
-        if logging.getLogger().level > self.logger.level:
-            # if the root logger's level is higher, we need to adjust that too
-            logging.getLogger().setLevel(self.logger.level)
-        self.logger.debug("Log level configured to %s", log_level)
+        self._set_log_level_from_config(config)
         self.cache = mass.cache
         self.available = False
 
@@ -68,6 +58,31 @@ class Provider:
         is_removed will be set to True when the provider is removed from the configuration.
         """
 
+    async def update_config(self, config: ProviderConfig, changed_keys: set[str]) -> None:
+        """
+        Handle logic when the config is updated.
+
+        Override this method in your provider implementation if you need
+        to perform any additional setup logic after the provider is registered and
+        the self.config was loaded, and whenever the config changes.
+        """
+        # default implementation: perform a full reload on any config change
+        # override in your provider if you need more fine-grained control
+        # such as checking the changed_keys set and only reload when 'requires_reload' keys changed
+        if changed_keys == {f"values/{CONF_LOG_LEVEL}"}:
+            # only log level changed, no need to reload
+            self._set_log_level_from_config(config)
+        else:
+            self.logger.info(
+                "Config updated, reloading provider %s (instance_id=%s)",
+                self.domain,
+                self.instance_id,
+            )
+            task_id = f"provider_reload_{self.instance_id}"
+            self.mass.call_later(
+                1, self.mass.load_provider_config, config, self.instance_id, task_id=task_id
+            )
+
     async def on_mdns_service_state_change(
         self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None
     ) -> None:
@@ -128,12 +143,6 @@ class Provider:
         """Return the stage of this provider."""
         return self.manifest.stage
 
-    def update_config_value(self, key: str, value: Any, encrypted: bool = False) -> None:
-        """Update a config value."""
-        self.mass.config.set_raw_provider_config_value(self.instance_id, key, value, encrypted)
-        # also update the cached copy within the provider instance
-        self.config.values[key].value = value
-
     def unload_with_error(self, error: str) -> None:
         """Unload provider with error message."""
         self.mass.call_later(1, self.mass.unload_provider, self.instance_id, error)
@@ -163,3 +172,23 @@ class Provider:
             raise UnsupportedFeaturedException(
                 f"Provider {self.name} does not support feature {feature.name}"
             )
+
+    def _update_config_value(self, key: str, value: Any, encrypted: bool = False) -> None:
+        """Update a config value."""
+        self.mass.config.set_raw_provider_config_value(self.instance_id, key, value, encrypted)
+        # also update the cached copy within the provider instance
+        self.config.values[key].value = value
+
+    def _set_log_level_from_config(self, config: ProviderConfig) -> None:
+        """Set log level from config."""
+        mass_logger = logging.getLogger(MASS_LOGGER_NAME)
+        self.logger = mass_logger.getChild(self.domain)
+        log_level = str(config.get_value(CONF_LOG_LEVEL))
+        if log_level == "GLOBAL":
+            self.logger.setLevel(mass_logger.level)
+        else:
+            self.logger.setLevel(log_level)
+        if logging.getLogger().level > self.logger.level:
+            # if the root logger's level is higher, we need to adjust that too
+            logging.getLogger().setLevel(self.logger.level)
+        self.logger.debug("Log level configured to %s", log_level)
index 8f91ffd54d6262d4d847b4526d4d6f2d517fecaf..a3da28e647805b4b2e669e322f74a39eef3c9239 100644 (file)
@@ -93,11 +93,10 @@ class DemoPlayer(Player):
         # OPTIONAL
         # this method is optional and should be implemented if you need player specific
         # configuration entries. If you do not need player specific configuration entries,
-        # you can leave this method out completely to accept the default implementation.
-        # Please note that you need to call the super() method to get the default entries.
-        default_entries = await super().get_config_entries(action=action, values=values)
+        # you can leave this method out completely.
+        # note that the config controller will always add a set of default config entries
+        # if you want, you can override those by specifying the same key as a default entry.
         return [
-            *default_entries,
             # example of a player specific config entry
             # you can also override a default entry by specifying the same key
             # as a default entry, but with a different type or default value.
index f3b2791f61e8d4af1c176cd06f6a044ccad9d890..2193424b098823a276610f8d16bceffa0995240f 100644 (file)
@@ -9,15 +9,7 @@ from typing import TYPE_CHECKING, cast
 from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType
 from music_assistant_models.enums import ConfigEntryType, PlaybackState, PlayerFeature, PlayerType
 
-from music_assistant.constants import (
-    CONF_ENTRY_DEPRECATED_EQ_BASS,
-    CONF_ENTRY_DEPRECATED_EQ_MID,
-    CONF_ENTRY_DEPRECATED_EQ_TREBLE,
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
-    CONF_ENTRY_OUTPUT_CODEC_HIDDEN,
-    CONF_ENTRY_SYNC_ADJUST,
-    create_sample_rates_config_entry,
-)
+from music_assistant.constants import CONF_ENTRY_SYNC_ADJUST, create_sample_rates_config_entry
 from music_assistant.models.player import DeviceInfo, Player, PlayerMedia
 
 from .constants import (
@@ -131,6 +123,11 @@ class AirPlayPlayer(Player):
                 return False
         return super().available
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     @property
     def corrected_elapsed_time(self) -> float:
         """Return the corrected elapsed time accounting for stream session restarts."""
@@ -149,8 +146,7 @@ class AirPlayPlayer(Player):
         values: dict[str, ConfigValueType] | None = None,
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the given player (if any)."""
-        base_entries = await super().get_config_entries()
-
+        base_entries: list[ConfigEntry] = []
         require_pairing = self._requires_pairing()
 
         # Handle pairing actions
@@ -159,15 +155,10 @@ class AirPlayPlayer(Player):
 
         # Add pairing config entries for Apple TV and macOS devices
         if require_pairing:
-            base_entries = [*self._get_pairing_config_entries(values), *base_entries]
+            base_entries = [*self._get_pairing_config_entries(values)]
 
         # Regular AirPlay config entries
         base_entries += [
-            CONF_ENTRY_FLOW_MODE_ENFORCED,
-            CONF_ENTRY_DEPRECATED_EQ_BASS,
-            CONF_ENTRY_DEPRECATED_EQ_MID,
-            CONF_ENTRY_DEPRECATED_EQ_TREBLE,
-            CONF_ENTRY_OUTPUT_CODEC_HIDDEN,
             ConfigEntry(
                 key=CONF_AIRPLAY_PROTOCOL,
                 type=ConfigEntryType.INTEGER,
index 752811f7beb14725afd36446a7ef1fe1753abe6a..a5b351642b35d6b41f08f302b6a76c572dc42901 100644 (file)
@@ -21,14 +21,7 @@ from music_assistant_models.enums import (
 from music_assistant_models.errors import ActionUnavailable, LoginFailed
 from music_assistant_models.player import DeviceInfo, PlayerMedia
 
-from music_assistant.constants import (
-    CONF_ENTRY_CROSSFADE_DURATION,
-    CONF_ENTRY_DEPRECATED_CROSSFADE,
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
-    CONF_ENTRY_HTTP_PROFILE,
-    CONF_PASSWORD,
-    CONF_USERNAME,
-)
+from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME
 from music_assistant.helpers.auth import AuthenticationHelper
 from music_assistant.models.player import Player
 from music_assistant.models.player_provider import PlayerProvider
@@ -282,6 +275,11 @@ class AlexaPlayer(Player):
         self._attr_powered = False
         self._attr_available = True
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     @property
     def api(self) -> AlexaAPI:
         """Get the AlexaAPI instance for this player."""
@@ -379,21 +377,6 @@ class AlexaPlayer(Player):
         self._attr_current_media = media
         self.update_state()
 
-    async def get_config_entries(
-        self,
-        action: str | None = None,
-        values: dict[str, ConfigValueType] | None = None,
-    ) -> list[ConfigEntry]:
-        """Return all (provider/player specific) Config Entries for the given player (if any)."""
-        base_entries = await super().get_config_entries(action=action, values=values)
-        return [
-            *base_entries,
-            CONF_ENTRY_FLOW_MODE_ENFORCED,
-            CONF_ENTRY_DEPRECATED_CROSSFADE,
-            CONF_ENTRY_CROSSFADE_DURATION,
-            CONF_ENTRY_HTTP_PROFILE,
-        ]
-
 
 class AlexaProvider(PlayerProvider):
     """Implementation of an Alexa Device Provider."""
index f9db930ae32db725ba4fb7db5a104e78f377b7c3..192137d461f28bcc2a7790834c43ed6e16c03497 100644 (file)
@@ -263,10 +263,10 @@ class ARDAudiothek(MusicProvider):
             self.token, self.user_id, _display_name = await _login(
                 self.mass.http_session, str(_email), str(_password)
             )
-            self.update_config_value(CONF_TOKEN_BEARER, self.token, encrypted=True)
-            self.update_config_value(CONF_USERID, self.user_id, encrypted=True)
-            self.update_config_value(CONF_DISPLAY_NAME, _display_name)
-            self.update_config_value(
+            self._update_config_value(CONF_TOKEN_BEARER, self.token, encrypted=True)
+            self._update_config_value(CONF_USERID, self.user_id, encrypted=True)
+            self._update_config_value(CONF_DISPLAY_NAME, _display_name)
+            self._update_config_value(
                 CONF_EXPIRY_TIME, str((datetime.now() + timedelta(hours=1)).timestamp())
             )
             self._client_initialized = False
index abad06f0c5d21e55fb11cc2e9ee675568b0fb30c..bd6a5560f718b30fed620a3d97e6dd55b95fec0d 100644 (file)
@@ -6,7 +6,6 @@ import asyncio
 import time
 from typing import TYPE_CHECKING
 
-from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
 from music_assistant_models.enums import PlaybackState, PlayerFeature, PlayerType
 from music_assistant_models.errors import PlayerCommandFailed
 from pyblu import Player as BluosPlayer
@@ -15,10 +14,8 @@ from pyblu.entities import Input, PairedPlayer, Preset
 from pyblu.errors import PlayerUnexpectedResponseError, PlayerUnreachableError
 
 from music_assistant.constants import (
-    CONF_ENTRY_ENABLE_ICY_METADATA,
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
     CONF_ENTRY_HTTP_PROFILE_DEFAULT_3,
-    CONF_ENTRY_OUTPUT_CODEC,
+    CONF_ENTRY_ICY_METADATA_DEFAULT_FULL,
     create_sample_rates_config_entry,
 )
 from music_assistant.models.player import DeviceInfo, Player, PlayerMedia, PlayerSource
@@ -34,6 +31,8 @@ from music_assistant.providers.bluesound.const import (
 )
 
 if TYPE_CHECKING:
+    from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
+
     from .provider import BluesoundDiscoveryInfo, BluesoundPlayerProvider
 
 
@@ -76,6 +75,11 @@ class BluesoundPlayer(Player):
         self._attr_poll_interval = IDLE_POLL_INTERVAL
         self._attr_can_group_with = {provider.instance_id}
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     async def setup(self) -> None:
         """Set up the player."""
         # Add volume support if available
@@ -91,7 +95,6 @@ class BluesoundPlayer(Player):
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the player."""
         return [
-            *await super().get_config_entries(action=action, values=values),
             CONF_ENTRY_HTTP_PROFILE_DEFAULT_3,
             create_sample_rates_config_entry(
                 max_sample_rate=192000,
@@ -99,11 +102,7 @@ class BluesoundPlayer(Player):
                 max_bit_depth=24,
                 safe_max_bit_depth=24,
             ),
-            CONF_ENTRY_OUTPUT_CODEC,
-            CONF_ENTRY_FLOW_MODE_ENFORCED,
-            ConfigEntry.from_dict(
-                {**CONF_ENTRY_ENABLE_ICY_METADATA.to_dict(), "default_value": "full"}
-            ),
+            CONF_ENTRY_ICY_METADATA_DEFAULT_FULL,
         ]
 
     async def disconnect(self) -> None:
index b97cdfde7ebee5aa22df2811555d39ac0467701c..580a95ff1b47bbe0e44dcb5a5bb4bfded3fb9d82 100644 (file)
@@ -332,7 +332,7 @@ class BuiltinProvider(MusicProvider):
         if media_type == MediaType.PLAYLIST and prov_item_id in BUILTIN_PLAYLISTS:
             # user wants to disable/remove one of our builtin playlists
             # to prevent it comes back, we mark it as disabled in config
-            self.update_config_value(prov_item_id, False)
+            self._update_config_value(prov_item_id, False)
             return True
         if media_type == MediaType.TRACK:
             # regular manual track URL/path
index ace7a5f10a1509b868aebdc8f8182f89092a41ee..115756166f16737e684da0f9c220cb5ba4fec752 100644 (file)
@@ -233,13 +233,6 @@ class ChromecastPlayer(Player):
         values: dict[str, ConfigValueType] | None = None,
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the given player (if any)."""
-        base_entries = await super().get_config_entries(action=action, values=values)
-
-        # Check if Sendspin provider is available
-        sendspin_available = any(
-            prov.domain == "sendspin" for prov in self.mass.get_providers("player")
-        )
-
         # Sendspin mode config entry
         sendspin_config = ConfigEntry(
             key=CONF_USE_SENDSPIN_MODE,
@@ -252,7 +245,7 @@ class ChromecastPlayer(Player):
             "NOTE: Requires the Sendspin provider to be enabled.",
             required=False,
             default_value=False,
-            hidden=not sendspin_available or self.type == PlayerType.GROUP,
+            hidden=self.type == PlayerType.GROUP,
         )
 
         # Sync delay config entry (only visible when sendspin provider is available)
@@ -267,7 +260,7 @@ class ChromecastPlayer(Player):
             required=False,
             default_value=DEFAULT_SENDSPIN_SYNC_DELAY,
             range=(-1000, 1000),
-            hidden=not sendspin_available or self.type == PlayerType.GROUP,
+            hidden=self.type == PlayerType.GROUP,
             immediate_apply=True,
         )
 
@@ -287,18 +280,16 @@ class ChromecastPlayer(Player):
                 ConfigValueOption("Opus (lossy, experimental)", "opus"),
                 ConfigValueOption("PCM (lossless, uncompressed)", "pcm"),
             ],
-            hidden=not sendspin_available or self.type == PlayerType.GROUP,
+            hidden=self.type == PlayerType.GROUP,
         )
 
         if self.type == PlayerType.GROUP:
             return [
-                *base_entries,
                 *CAST_PLAYER_CONFIG_ENTRIES,
                 CONF_ENTRY_SAMPLE_RATES_CAST_GROUP,
             ]
 
         return [
-            *base_entries,
             *CAST_PLAYER_CONFIG_ENTRIES,
             CONF_ENTRY_SAMPLE_RATES_CAST,
             sendspin_config,
index f51d87423818c1d1cc738005d9020610ae1b77b6..cf8fd27c4de73ea3f81decc38f984eacf2861260 100644 (file)
@@ -1,20 +1,13 @@
 """Constants for DLNA provider."""
 
-from music_assistant.constants import (
-    CONF_ENTRY_ENABLE_ICY_METADATA,
-    CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED,
-    CONF_ENTRY_HTTP_PROFILE,
-    CONF_ENTRY_OUTPUT_CODEC,
-    create_sample_rates_config_entry,
-)
+from music_assistant_models.config_entries import ConfigEntry
+
+from music_assistant.constants import CONF_ENTRY_FLOW_MODE, create_sample_rates_config_entry
 
 PLAYER_CONFIG_ENTRIES = [
-    CONF_ENTRY_OUTPUT_CODEC,
-    CONF_ENTRY_HTTP_PROFILE,
-    CONF_ENTRY_ENABLE_ICY_METADATA,
     # enable flow mode by default because
     # most dlna players do not support enqueueing
-    CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED,
+    ConfigEntry.from_dict({**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True}),
     create_sample_rates_config_entry(max_sample_rate=192000, max_bit_depth=24),
 ]
 
index 268bbf4146649938b7d5ad01190d8ef132b09f02..e230ff08622d107ac332f269c58e79a19136b784 100644 (file)
@@ -268,8 +268,7 @@ class DLNAPlayer(Player):
         values: dict[str, ConfigValueType] | None = None,
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the given player (if any)."""
-        base_entries = await super().get_config_entries(action=action, values=values)
-        return base_entries + PLAYER_CONFIG_ENTRIES
+        return [*PLAYER_CONFIG_ENTRIES]
 
     # async def on_player_config_change(
     #     self,
index d1b30d1400da3ac276f70db7fe400bafbf5389fa..30622d042d73c807cd60c3b5a32120a07c7d9a90 100644 (file)
@@ -9,11 +9,7 @@ from typing import TYPE_CHECKING
 from music_assistant_models.enums import PlaybackState, PlayerFeature, PlayerType
 from music_assistant_models.errors import PlayerCommandFailed, PlayerUnavailableError
 
-from music_assistant.constants import (
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
-    CONF_ENTRY_HTTP_PROFILE,
-    CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
-)
+from music_assistant.constants import CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3
 from music_assistant.models.player import DeviceInfo, Player, PlayerMedia
 
 if TYPE_CHECKING:
@@ -51,18 +47,19 @@ class FullyKioskPlayer(Player):
         self._attr_needs_poll = True
         self._attr_poll_interval = 10
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     async def get_config_entries(
         self,
         action: str | None = None,
         values: dict[str, ConfigValueType] | None = None,
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the given player (if any)."""
-        base_entries = await super().get_config_entries(action=action, values=values)
         return [
-            *base_entries,
-            CONF_ENTRY_FLOW_MODE_ENFORCED,
             CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
-            CONF_ENTRY_HTTP_PROFILE,
         ]
 
     def set_attributes(self) -> None:
index 3918fa002e8bdeef5e99da1399768ac966b1b45e..44e80d248e1dea2f4ce189d757c713a0b8dae276 100644 (file)
@@ -269,7 +269,7 @@ class GPodder(MusicProvider):
             assert nc_url is not None
             self._client.init_nc(base_url=nc_url, nc_token=str(nc_token))
         else:
-            self.update_config_value(CONF_USING_GPODDER, True)
+            self._update_config_value(CONF_USING_GPODDER, True)
             if _username is None or _password is None or _device_id is None:
                 raise LoginFailed("Must provide username, password and device_id.")
             username = str(_username)
index 2e16aaaec7a7f4b1b8a652c5ebf9c714fc7c1b1b..4f3f6f9b713e8cdcc90779cbb14366c9a554dea1 100644 (file)
@@ -10,11 +10,7 @@ from hass_client.exceptions import FailedCommand
 from music_assistant_models.enums import PlaybackState, PlayerFeature, PlayerType
 
 from music_assistant.constants import (
-    CONF_ENTRY_ENABLE_ICY_METADATA,
     CONF_ENTRY_ENABLE_ICY_METADATA_HIDDEN,
-    CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED,
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
-    CONF_ENTRY_HTTP_PROFILE,
     CONF_ENTRY_HTTP_PROFILE_FORCED_2,
     CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
     HIDDEN_ANNOUNCE_VOLUME_CONFIG_ENTRIES,
@@ -43,12 +39,7 @@ if TYPE_CHECKING:
     from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
 
 
-DEFAULT_PLAYER_CONFIG_ENTRIES = (
-    CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
-    CONF_ENTRY_HTTP_PROFILE,
-    CONF_ENTRY_ENABLE_ICY_METADATA,
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
-)
+DEFAULT_PLAYER_CONFIG_ENTRIES = (CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,)
 
 
 class HomeAssistantPlayer(Player):
@@ -106,14 +97,19 @@ class HomeAssistantPlayer(Player):
         self.extra_data["hass_supported_features"] = hass_supported_features
         self._update_attributes(hass_state["attributes"])
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        # hass media players are a hot mess so play it safe and always use flow mode
+        return True
+
     async def get_config_entries(
         self,
         action: str | None = None,
         values: dict[str, ConfigValueType] | None = None,
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the player."""
-        base_entries = await super().get_config_entries(action=action, values=values)
-        base_entries = [*base_entries, *DEFAULT_PLAYER_CONFIG_ENTRIES]
+        base_entries = [*DEFAULT_PLAYER_CONFIG_ENTRIES]
         if self.extra_data.get("esphome_supported_audio_formats"):
             # optimized config for new ESPHome mediaplayer
             supported_sample_rates: list[int] = []
@@ -141,7 +137,6 @@ class HomeAssistantPlayer(Player):
             config_entries = [
                 *base_entries,
                 # New ESPHome mediaplayer (used in Voice PE) uses FLAC 48khz/16 bits
-                CONF_ENTRY_FLOW_MODE_ENFORCED,
                 CONF_ENTRY_HTTP_PROFILE_FORCED_2,
             ]
 
@@ -168,10 +163,6 @@ class HomeAssistantPlayer(Player):
         if self.extra_data.get("hass_domain") in WARN_HASS_INTEGRATIONS:
             base_entries = [CONF_ENTRY_WARN_HASS_INTEGRATION, *base_entries]
 
-        # enable flow mode by default if player does not report enqueue support
-        if MediaPlayerEntityFeature.MEDIA_ENQUEUE not in self.extra_data["hass_supported_features"]:
-            base_entries = [*base_entries, CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED]
-
         return base_entries
 
     async def play(self) -> None:
index fb0a878ebc51e11920efe22eee5e25e04b77448a..783d792e6682abda07101ee3cf54d13cb2a415a0 100644 (file)
@@ -10,17 +10,11 @@ from music_assistant_models.errors import SetupFailedError
 from music_assistant_models.player import DeviceInfo, PlayerSource
 from pyheos import Heos, const
 
-from music_assistant.constants import (
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
-    create_sample_rates_config_entry,
-)
+from music_assistant.constants import create_sample_rates_config_entry
 from music_assistant.models.player import Player, PlayerMedia
 from music_assistant.providers.heos.helpers import media_uri_from_now_playing_media
 
-from .constants import (
-    HEOS_MEDIA_TYPE_TO_MEDIA_TYPE,
-    HEOS_PLAY_STATE_TO_PLAYBACK_STATE,
-)
+from .constants import HEOS_MEDIA_TYPE_TO_MEDIA_TYPE, HEOS_PLAY_STATE_TO_PLAYBACK_STATE
 
 if TYPE_CHECKING:
     from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
@@ -45,6 +39,11 @@ class HeosPlayer(Player):
     _heos: Heos
     _device: PyHeosPlayer
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     def __init__(self, provider: HeosPlayerProvider, device: PyHeosPlayer) -> None:
         """Initialize the Player."""
         super().__init__(provider, str(device.player_id))
@@ -296,7 +295,6 @@ class HeosPlayer(Player):
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the player."""
         return [
-            *await super().get_config_entries(action=action, values=values),
             # Gen 1 devices, like HEOS Link, only support up to 48kHz/16bit
             create_sample_rates_config_entry(
                 max_sample_rate=192000,
@@ -304,5 +302,4 @@ class HeosPlayer(Player):
                 max_bit_depth=24,
                 safe_max_bit_depth=16,
             ),
-            CONF_ENTRY_FLOW_MODE_ENFORCED,
         ]
index f680ef88c69a4869f2c1909d8ad00c699b385e57..b9aeaede249319cb65e1cb381b0d86dacd891a43 100644 (file)
@@ -1,17 +1,25 @@
 """Constants for the MusicCast provider."""
 
+from music_assistant_models.config_entries import ConfigEntry
+
 from music_assistant.constants import (
-    CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
+    CONF_ENTRY_FLOW_MODE,
     CONF_ENTRY_HTTP_PROFILE_DEFAULT_2,
     CONF_ENTRY_ICY_METADATA_HIDDEN_DISABLED,
-    CONF_ENTRY_OUTPUT_CODEC,
     create_sample_rates_config_entry,
 )
 
 # Constants for players
 # both the http profile and icy didn't matter for me testing it.
+CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED = ConfigEntry.from_dict(
+    {
+        **CONF_ENTRY_FLOW_MODE.to_dict(),
+        "default_value": False,
+        "value": False,
+        "hidden": True,
+    }
+)
 PLAYER_CONFIG_ENTRIES = [
-    CONF_ENTRY_OUTPUT_CODEC,
     CONF_ENTRY_HTTP_PROFILE_DEFAULT_2,
     CONF_ENTRY_ICY_METADATA_HIDDEN_DISABLED,
     CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
index b2d02a17e2bd8ea84a66a233fc37d302401f13bc..0cf41fba5d5c927216461049b8fcf51c00cd9c68 100644 (file)
@@ -173,7 +173,7 @@ class PodcastIndexProvider(MusicProvider):
 
         self.logger.debug("Adding podcast %s to library", item.name)
         stored_podcasts.append(feed_url)
-        self.update_config_value(CONF_STORED_PODCASTS, stored_podcasts)
+        self._update_config_value(CONF_STORED_PODCASTS, stored_podcasts)
         return True
 
     async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
@@ -205,7 +205,7 @@ class PodcastIndexProvider(MusicProvider):
 
         self.logger.debug("Removing podcast %s from library", prov_item_id)
         stored_podcasts = [x for x in stored_podcasts if x != feed_url]
-        self.update_config_value(CONF_STORED_PODCASTS, stored_podcasts)
+        self._update_config_value(CONF_STORED_PODCASTS, stored_podcasts)
         return True
 
     @use_cache(3600 * 24 * 14)  # Cache for 14 days
index c8fd2635931d503f144d858cb9a070025ddc7766..d384d483a941828219d11c810bd76c6683242dd6 100644 (file)
@@ -270,7 +270,7 @@ class RadioBrowserProvider(MusicProvider):
             return False
         self.logger.debug("Adding radio %s to stored radios", item.item_id)
         stored_radios = [*stored_radios, item.item_id]
-        self.update_config_value(CONF_STORED_RADIOS, stored_radios)
+        self._update_config_value(CONF_STORED_RADIOS, stored_radios)
         return True
 
     async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
@@ -282,7 +282,7 @@ class RadioBrowserProvider(MusicProvider):
             return False
         self.logger.debug("Removing radio %s from stored radios", prov_item_id)
         stored_radios = [x for x in stored_radios if x != prov_item_id]
-        self.update_config_value(CONF_STORED_RADIOS, stored_radios)
+        self._update_config_value(CONF_STORED_RADIOS, stored_radios)
         return True
 
     @use_cache(3600 * 6)  # Cache for 6 hours
index 6e4625f76eed8dc0af5ef6dcc127b3b9d8a81286..2a64524f8acdd8f38f145d3be710c60e1235922c 100644 (file)
@@ -29,7 +29,6 @@ from aiosendspin.server.group import (
 )
 from aiosendspin.server.metadata import Metadata
 from aiosendspin.server.stream import AudioCodec, MediaStream
-from music_assistant_models.config_entries import ConfigEntry
 from music_assistant_models.constants import PLAYER_CONTROL_NONE
 from music_assistant_models.enums import (
     ContentType,
@@ -43,15 +42,7 @@ from music_assistant_models.media_items import AudioFormat
 from music_assistant_models.player import DeviceInfo
 from PIL import Image
 
-from music_assistant.constants import (
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
-    CONF_ENTRY_HTTP_PROFILE_HIDDEN,
-    CONF_ENTRY_OUTPUT_CODEC_HIDDEN,
-    CONF_ENTRY_SAMPLE_RATES,
-    CONF_OUTPUT_CHANNELS,
-    CONF_OUTPUT_CODEC,
-    INTERNAL_PCM_FORMAT,
-)
+from music_assistant.constants import CONF_OUTPUT_CHANNELS, CONF_OUTPUT_CODEC, INTERNAL_PCM_FORMAT
 from music_assistant.helpers.audio import get_player_filter_params
 from music_assistant.models.player import Player, PlayerMedia
 
@@ -73,7 +64,6 @@ SUPPORTED_GROUP_COMMANDS = [
 
 if TYPE_CHECKING:
     from aiosendspin.server.client import SendspinClient
-    from music_assistant_models.config_entries import ConfigValueType
     from music_assistant_models.player_queue import PlayerQueue
     from music_assistant_models.queue_item import QueueItem
 
@@ -114,6 +104,11 @@ class MusicAssistantMediaStream(MediaStream):
         self.internal_format = internal_format
         self.output_format = output_format
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     async def player_channel(
         self,
         player_id: str,
@@ -191,6 +186,11 @@ class SendspinPlayer(Player):
     timed_client_stream: TimedClientStream | None = None
     is_web_player: bool = False
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     def __init__(self, provider: SendspinProvider, player_id: str) -> None:
         """Initialize the Player."""
         super().__init__(provider, player_id)
@@ -595,21 +595,6 @@ class SendspinPlayer(Player):
         # Send metadata to the group
         self.api.group.set_metadata(metadata)
 
-    async def get_config_entries(
-        self,
-        action: str | None = None,
-        values: dict[str, ConfigValueType] | None = None,
-    ) -> list[ConfigEntry]:
-        """Return all (provider/player specific) Config Entries for the player."""
-        default_entries = await super().get_config_entries(action=action, values=values)
-        return [
-            *default_entries,
-            CONF_ENTRY_FLOW_MODE_ENFORCED,
-            CONF_ENTRY_OUTPUT_CODEC_HIDDEN,
-            CONF_ENTRY_HTTP_PROFILE_HIDDEN,
-            ConfigEntry.from_dict({**CONF_ENTRY_SAMPLE_RATES.to_dict(), "hidden": True}),
-        ]
-
     async def on_unload(self) -> None:
         """Handle logic when the player is unloaded from the Player controller."""
         await super().on_unload()
index 5e2ef7d0d5290d9602bcd0573518c0fe52056a56..221a3f74c605231b9a113303a78d67cbc5226bc1 100644 (file)
@@ -14,11 +14,7 @@ from snapcast.control.client import Snapclient
 from snapcast.control.group import Snapgroup
 from snapcast.control.stream import Snapstream
 
-from music_assistant.constants import (
-    ATTR_ANNOUNCEMENT_IN_PROGRESS,
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
-    CONF_ENTRY_OUTPUT_CODEC_HIDDEN,
-)
+from music_assistant.constants import ATTR_ANNOUNCEMENT_IN_PROGRESS
 from music_assistant.helpers.audio import get_player_filter_params
 from music_assistant.helpers.compare import create_safe_string
 from music_assistant.helpers.ffmpeg import FFMpeg
@@ -52,6 +48,11 @@ class SnapCastPlayer(Player):
         super().__init__(provider, player_id)
         self._stream_task: asyncio.Task[None] | None = None
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     @property
     def synced_to(self) -> str | None:
         """
@@ -292,12 +293,8 @@ class SnapCastPlayer(Player):
         values: dict[str, ConfigValueType] | None = None,
     ) -> list[ConfigEntry]:
         """Player config."""
-        base_entries = await super().get_config_entries(action=action, values=values)
         return [
-            *base_entries,
-            CONF_ENTRY_FLOW_MODE_ENFORCED,
             CONF_ENTRY_SAMPLE_RATES_SNAPCAST,
-            CONF_ENTRY_OUTPUT_CODEC_HIDDEN,
         ]
 
     def _handle_player_update(self, snap_client: Snapclient) -> None:
index 02db85f8b9ea4533053fb16edd522d4f68268174..4417fc6c7ab8f9b2f89cb8da8acf8ef82b59a902 100644 (file)
@@ -34,7 +34,6 @@ from music_assistant_models.player import PlayerMedia
 
 from music_assistant.constants import (
     CONF_ENTRY_HTTP_PROFILE_DEFAULT_2,
-    CONF_ENTRY_OUTPUT_CODEC,
     create_sample_rates_config_entry,
 )
 from music_assistant.helpers.tags import async_parse_tags
@@ -197,8 +196,6 @@ class SonosPlayer(Player):
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the player."""
         base_entries = [
-            *await super().get_config_entries(action=action, values=values),
-            CONF_ENTRY_OUTPUT_CODEC,
             CONF_ENTRY_HTTP_PROFILE_DEFAULT_2,
             create_sample_rates_config_entry(
                 # set safe max bit depth to 16 bits because the older Sonos players
index 6cf87fd41c3702c7b77b8314888fa2e88ea5c51b..1e4e57e29fa77b7c4cdc257fb3ce74aaf8aa876f 100644 (file)
@@ -20,13 +20,7 @@ from soco import SoCoException
 from soco.core import MUSIC_SRC_RADIO, SoCo
 from soco.data_structures import DidlAudioBroadcast
 
-from music_assistant.constants import (
-    CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
-    CONF_ENTRY_HTTP_PROFILE_DEFAULT_1,
-    CONF_ENTRY_OUTPUT_CODEC,
-    VERBOSE_LOG_LEVEL,
-    create_sample_rates_config_entry,
-)
+from music_assistant.constants import VERBOSE_LOG_LEVEL, create_sample_rates_config_entry
 from music_assistant.helpers.upnp import create_didl_metadata
 from music_assistant.models.player import DeviceInfo, Player, PlayerMedia
 
@@ -131,14 +125,10 @@ class SonosPlayer(Player):
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the player."""
         return [
-            *await super().get_config_entries(action=action, values=values),
-            CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
-            CONF_ENTRY_HTTP_PROFILE_DEFAULT_1,
-            CONF_ENTRY_OUTPUT_CODEC,
             create_sample_rates_config_entry(
                 supported_sample_rates=[44100, 48000],
                 supported_bit_depths=[16],
-                hidden=False,
+                hidden=True,
             ),
         ]
 
index f74cc79f09bd272749e061f8353c00d83d1aaceb..bc1ddeb41aff22f37376fa38fe21530d3f987d7b 100644 (file)
@@ -830,7 +830,7 @@ class SpotifyProvider(MusicProvider):
         except LoginFailed as err:
             if "revoked" in str(err):
                 # clear refresh token if it's invalid
-                self.update_config_value(CONF_REFRESH_TOKEN_GLOBAL, None)
+                self._update_config_value(CONF_REFRESH_TOKEN_GLOBAL, None)
                 if self.available:
                     self.unload_with_error(str(err))
             elif self.available:
@@ -841,7 +841,7 @@ class SpotifyProvider(MusicProvider):
 
         # make sure that our updated creds get stored in memory + config
         self._auth_info_global = auth_info
-        self.update_config_value(
+        self._update_config_value(
             CONF_REFRESH_TOKEN_GLOBAL, auth_info["refresh_token"], encrypted=True
         )
 
@@ -889,8 +889,8 @@ class SpotifyProvider(MusicProvider):
         except LoginFailed as err:
             if "revoked" in str(err):
                 # clear refresh token if it's invalid
-                self.update_config_value(CONF_REFRESH_TOKEN_DEV, None)
-                self.update_config_value(CONF_CLIENT_ID, None)
+                self._update_config_value(CONF_REFRESH_TOKEN_DEV, None)
+                self._update_config_value(CONF_CLIENT_ID, None)
             # Don't unload - we can still use the global session
             self.dev_session_active = False
             self.logger.warning(str(err))
@@ -898,7 +898,9 @@ class SpotifyProvider(MusicProvider):
 
         # make sure that our updated creds get stored in memory + config
         self._auth_info_dev = auth_info
-        self.update_config_value(CONF_REFRESH_TOKEN_DEV, auth_info["refresh_token"], encrypted=True)
+        self._update_config_value(
+            CONF_REFRESH_TOKEN_DEV, auth_info["refresh_token"], encrypted=True
+        )
 
         # Setup librespot with dev token (preferred over global token)
         await self._setup_librespot_auth(auth_info["access_token"])
index 9e3d7c35e97e6137fc85c4a74166222cd9e4a02f..2a7287e20097590711fefb08e5541e1fe02cfbba 100644 (file)
@@ -27,11 +27,7 @@ from music_assistant_models.errors import InvalidCommand, MusicAssistantError
 from music_assistant_models.media_items import AudioFormat
 
 from music_assistant.constants import (
-    CONF_ENTRY_DEPRECATED_EQ_BASS,
-    CONF_ENTRY_DEPRECATED_EQ_MID,
-    CONF_ENTRY_DEPRECATED_EQ_TREBLE,
     CONF_ENTRY_HTTP_PROFILE_FORCED_2,
-    CONF_ENTRY_OUTPUT_CODEC,
     CONF_ENTRY_SUPPORT_GAPLESS_DIFFERENT_SAMPLE_RATES,
     CONF_ENTRY_SYNC_ADJUST,
     INTERNAL_PCM_FORMAT,
@@ -158,10 +154,6 @@ class SqueezelitePlayer(Player):
         return [
             *base_entries,
             *preset_entries,
-            CONF_ENTRY_DEPRECATED_EQ_BASS,
-            CONF_ENTRY_DEPRECATED_EQ_MID,
-            CONF_ENTRY_DEPRECATED_EQ_TREBLE,
-            CONF_ENTRY_OUTPUT_CODEC,
             CONF_ENTRY_SYNC_ADJUST,
             CONF_ENTRY_DISPLAY,
             CONF_ENTRY_VISUALIZATION,
index d263c47832074196be3ed6a81c0b1e4fbfe72d44..5ab39c9dea820eee05a13e69291cdfa9aec36d78 100644 (file)
@@ -88,10 +88,10 @@ class TidalProvider(MusicProvider):
 
     def _update_auth_config(self, auth_info: dict[str, Any]) -> None:
         """Update auth config with new auth info."""
-        self.update_config_value(CONF_AUTH_TOKEN, auth_info["access_token"], encrypted=True)
-        self.update_config_value(CONF_REFRESH_TOKEN, auth_info["refresh_token"], encrypted=True)
-        self.update_config_value(CONF_EXPIRY_TIME, auth_info["expires_at"])
-        self.update_config_value(CONF_USER_ID, auth_info["userId"])
+        self._update_config_value(CONF_AUTH_TOKEN, auth_info["access_token"], encrypted=True)
+        self._update_config_value(CONF_REFRESH_TOKEN, auth_info["refresh_token"], encrypted=True)
+        self._update_config_value(CONF_EXPIRY_TIME, auth_info["expires_at"])
+        self._update_config_value(CONF_USER_ID, auth_info["userId"])
 
     async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
@@ -107,7 +107,7 @@ class TidalProvider(MusicProvider):
             try:
                 dt = datetime.fromisoformat(expires_at)
                 expires_at = dt.timestamp()
-                self.update_config_value(CONF_EXPIRY_TIME, expires_at)
+                self._update_config_value(CONF_EXPIRY_TIME, expires_at)
             except ValueError:
                 expires_at = 0
 
index b1158820e5b77368e8c8365779f9d5ff3dba9961..de9d370cb4511c5c1ab4cdb7b354fa9930be24e6 100644 (file)
@@ -24,7 +24,6 @@ from propcache import under_cached_property as cached_property
 
 from music_assistant.constants import (
     CONF_DYNAMIC_GROUP_MEMBERS,
-    CONF_ENTRY_FLOW_MODE_ENFORCED,
     CONF_GROUP_MEMBERS,
     CONF_HTTP_PROFILE,
     DEFAULT_STREAM_HEADERS,
@@ -85,6 +84,11 @@ class UniversalGroupPlayer(GroupPlayer):
         }
         self._set_attributes()
 
+    @property
+    def requires_flow_mode(self) -> bool:
+        """Return if the player requires flow mode."""
+        return True
+
     async def on_config_updated(self) -> None:
         """Handle logic when the player is loaded or updated."""
         static_members = cast("list[str]", self.config.get_value(CONF_GROUP_MEMBERS, []))
@@ -108,8 +112,6 @@ class UniversalGroupPlayer(GroupPlayer):
     ) -> list[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the given player (if any)."""
         return [
-            # default entries for player groups
-            *await super().get_config_entries(action=action, values=values),
             # add universal group specific entries
             CONFIG_ENTRY_UGP_NOTE,
             ConfigEntry(
@@ -135,7 +137,6 @@ class UniversalGroupPlayer(GroupPlayer):
                 required=False,
             ),
             CONF_ENTRY_SAMPLE_RATES_UGP,
-            CONF_ENTRY_FLOW_MODE_ENFORCED,
         ]
 
     async def stop(self) -> None: