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"
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"
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"
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",
)
default_value="stereo",
label="Output Channel Mode",
category="audio",
+ requires_reload=True,
)
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(
description="Adjust average (perceived) loudness to this target level",
depends_on=CONF_VOLUME_NORMALIZATION,
category="advanced",
+ requires_reload=True,
)
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,
"- 'Standard Crossfade': Regular crossfade that crossfades the last/first x-seconds of a "
"track.",
category="audio",
+ requires_reload=True,
)
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,
)
"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(
"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,
)
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,
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,
)
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(
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",
"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(
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,
"experience audio glitches during transitioning between tracks.",
default_value=False,
category="advanced",
+ requires_reload=True,
)
CONF_ENTRY_WARN_PREVIEW = 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,
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")
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,
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,
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,
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
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(
# 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}"
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:
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")
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:
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")):
"""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
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
# 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,
+ ]
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."""
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
) -> 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,
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)
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
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):
"""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:
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:
"""
@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
"""
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:
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):
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,
# 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.
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
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:
"""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)
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)
# 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.
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 (
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."""
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
# 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,
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
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."""
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."""
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
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
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
)
if TYPE_CHECKING:
+ from music_assistant_models.config_entries import ConfigEntry, ConfigValueType
+
from .provider import BluesoundDiscoveryInfo, BluesoundPlayerProvider
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
) -> 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,
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:
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
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,
"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)
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,
)
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,
"""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),
]
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,
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:
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:
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)
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,
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):
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] = []
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,
]
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:
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
_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))
) -> 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,
max_bit_depth=24,
safe_max_bit_depth=16,
),
- CONF_ENTRY_FLOW_MODE_ENFORCED,
]
"""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,
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:
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
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:
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
)
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,
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
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
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,
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)
# 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()
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
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:
"""
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:
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
) -> 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
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
) -> 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,
),
]
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:
# 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
)
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))
# 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"])
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,
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,
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."""
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
from music_assistant.constants import (
CONF_DYNAMIC_GROUP_MEMBERS,
- CONF_ENTRY_FLOW_MODE_ENFORCED,
CONF_GROUP_MEMBERS,
CONF_HTTP_PROFILE,
DEFAULT_STREAM_HEADERS,
}
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, []))
) -> 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(
required=False,
),
CONF_ENTRY_SAMPLE_RATES_UGP,
- CONF_ENTRY_FLOW_MODE_ENFORCED,
]
async def stop(self) -> None: