From: Marcel van der Veldt Date: Wed, 28 Aug 2024 11:48:11 +0000 (+0200) Subject: add config options for sync groups X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=886b72718b72c0cfafb3c46985167a37b2f44a81;p=music-assistant-server.git add config options for sync groups --- diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py index 2d8941d9..d4826fd4 100644 --- a/music_assistant/common/models/config_entries.py +++ b/music_assistant/common/models/config_entries.py @@ -645,3 +645,17 @@ def create_sample_rates_config_entry( conf_entry.options = tuple(options) conf_entry.default_value = default_value return conf_entry + + +BASE_PLAYER_CONFIG_ENTRIES = ( + # config entries that are valid for all players + CONF_ENTRY_PLAYER_ICON, + CONF_ENTRY_FLOW_MODE, + CONF_ENTRY_VOLUME_NORMALIZATION, + CONF_ENTRY_AUTO_PLAY, + CONF_ENTRY_VOLUME_NORMALIZATION_TARGET, + CONF_ENTRY_HIDE_PLAYER, + CONF_ENTRY_TTS_PRE_ANNOUNCE, + CONF_ENTRY_SAMPLE_RATES, + CONF_ENTRY_HTTP_PROFILE_FORCED_1, +) diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 2a8418ee..3442884d 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -68,6 +68,8 @@ CONF_HTTP_PROFILE: Final[str] = "http_profile" CONF_SYNC_LEADER: Final[str] = "sync_leader" CONF_BYPASS_NORMALIZATION_RADIO: Final[str] = "bypass_normalization_radio" CONF_BYPASS_NORMALIZATION_SHORT: Final[str] = "bypass_normalization_short" +CONF_PREVENT_SYNC_LEADER_OFF: Final[str] = "prevent_sync_leader_off" +CONF_SYNCGROUP_DEFAULT_ON: Final[str] = "syncgroup_default_on" # config default values DEFAULT_HOST: Final[str] = "0.0.0.0" diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index 0e861801..6a62ef67 100644 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -44,7 +44,9 @@ from music_assistant.constants import ( CONF_GROUP_MEMBERS, CONF_HIDE_PLAYER, CONF_PLAYERS, + CONF_PREVENT_SYNC_LEADER_OFF, CONF_SYNC_LEADER, + CONF_SYNCGROUP_DEFAULT_ON, CONF_TTS_PRE_ANNOUNCE, SYNCGROUP_PREFIX, ) @@ -300,6 +302,21 @@ class PlayerController(CoreController): if active_group_player_id := self._get_active_player_group(player): active_group_player = self.get(active_group_player_id) group_player_state = active_group_player.state + if not powered and active_group_player.type == PlayerType.SYNC_GROUP: + # handle 'prevent sync leader off' feature + powered_members = list(self.iter_group_members(active_group_player, True)) + sync_leader = self.get_sync_leader(active_group_player) + if ( + len(powered_members) > 1 + and (sync_leader == player) + and self.mass.config.get_raw_player_config_value( + active_group_player_id, CONF_PREVENT_SYNC_LEADER_OFF, False + ) + ): + raise PlayerCommandFailed( + f"{player.display_name} is the sync " + "leader of a syncgroup and cannot be turned off" + ) else: active_group_player = None @@ -460,11 +477,17 @@ class PlayerController(CoreController): if not power and group_player.state in (PlayerState.PLAYING, PlayerState.PAUSED): await self.cmd_stop(player_id) + default_on_pref = self.mass.config.get_raw_player_config_value( + group_player.player_id, CONF_SYNCGROUP_DEFAULT_ON, "powered_only" + ) + # handle syncgroup - this will also work for temporary syncgroups # where players are manually synced against a group leader any_member_powered = False async with TaskManager(self.mass) as tg: - for member in self.iter_group_members(group_player, only_powered=True): + for member in self.iter_group_members( + group_player, only_powered=(default_on_pref != "always_all") + ): any_member_powered = True if power: if member.state in (PlayerState.PLAYING, PlayerState.PAUSED): @@ -481,12 +504,21 @@ class PlayerController(CoreController): member.active_source = None member.active_group = None self.update(member.player_id, skip_forward=True) - # edge case: group turned on but no members are powered, power them all! - if not any_member_powered and power: + # handle default power ON + if power: + sync_leader = self.get_sync_leader(group_player) for member in self.iter_group_members(group_player, only_powered=False): - tg.create_task(self.cmd_power(member.player_id, True)) - member.active_group = group_player.player_id - member.active_source = group_player.active_source + if default_on_pref == "always_all" or ( + sync_leader + and default_on_pref == "always_leader" + and member.player_id == sync_leader.player_id + ): + tg.create_task(self.cmd_power(member.player_id, True)) + member.active_group = group_player.player_id + member.active_source = group_player.active_source + any_member_powered = True + if not any_member_powered: + return if power and group_player.player_id.startswith(SYNCGROUP_PREFIX): await self.sync_syncgroup(group_player.player_id) diff --git a/music_assistant/server/models/player_provider.py b/music_assistant/server/models/player_provider.py index 48711dc8..f1646752 100644 --- a/music_assistant/server/models/player_provider.py +++ b/music_assistant/server/models/player_provider.py @@ -5,27 +5,25 @@ from __future__ import annotations from abc import abstractmethod from music_assistant.common.models.config_entries import ( + BASE_PLAYER_CONFIG_ENTRIES, 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_FLOW_MODE, - CONF_ENTRY_HIDE_PLAYER, - CONF_ENTRY_HTTP_PROFILE_FORCED_1, - CONF_ENTRY_PLAYER_ICON, CONF_ENTRY_PLAYER_ICON_GROUP, - CONF_ENTRY_SAMPLE_RATES, - CONF_ENTRY_TTS_PRE_ANNOUNCE, - CONF_ENTRY_VOLUME_NORMALIZATION, - CONF_ENTRY_VOLUME_NORMALIZATION_TARGET, ConfigEntry, ConfigValueOption, PlayerConfig, ) from music_assistant.common.models.enums import ConfigEntryType, PlayerState from music_assistant.common.models.player import Player, PlayerMedia -from music_assistant.constants import CONF_GROUP_MEMBERS, CONF_SYNC_LEADER, SYNCGROUP_PREFIX +from music_assistant.constants import ( + CONF_GROUP_MEMBERS, + CONF_PREVENT_SYNC_LEADER_OFF, + CONF_SYNC_LEADER, + CONF_SYNCGROUP_DEFAULT_ON, + SYNCGROUP_PREFIX, +) from .provider import Provider @@ -40,21 +38,11 @@ class PlayerProvider(Provider): async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry, ...]: """Return all (provider/player specific) Config Entries for the given player (if any).""" - entries = ( - CONF_ENTRY_PLAYER_ICON, - CONF_ENTRY_FLOW_MODE, - CONF_ENTRY_VOLUME_NORMALIZATION, - CONF_ENTRY_AUTO_PLAY, - CONF_ENTRY_VOLUME_NORMALIZATION_TARGET, - CONF_ENTRY_HIDE_PLAYER, - CONF_ENTRY_TTS_PRE_ANNOUNCE, - CONF_ENTRY_SAMPLE_RATES, - CONF_ENTRY_HTTP_PROFILE_FORCED_1, - ) if player_id.startswith(SYNCGROUP_PREFIX): - # add default entries for syncgroups - entries = ( - *entries, + # default entries for syncgroups + return ( + *BASE_PLAYER_CONFIG_ENTRIES, + CONF_ENTRY_PLAYER_ICON_GROUP, ConfigEntry( key=CONF_GROUP_MEMBERS, type=ConfigEntryType.STRING, @@ -93,18 +81,44 @@ class PlayerProvider(Provider): "the sync leader, select it here.", required=True, ), - CONF_ENTRY_PLAYER_ICON_GROUP, + ConfigEntry( + key=CONF_PREVENT_SYNC_LEADER_OFF, + type=ConfigEntryType.BOOLEAN, + label="Prevent sync leader power off", + default_value=False, + description="With this setting enabled, Music Assistant will disallow powering " + "off the sync leader player if other players are still " + "active in the sync group. This is useful if you want to prevent " + "a short drop in the music while the music is transferred to another player.", + required=True, + ), + ConfigEntry( + key=CONF_SYNCGROUP_DEFAULT_ON, + type=ConfigEntryType.STRING, + label="Default power ON behavior", + default_value="powered_only", + options=( + ConfigValueOption("Always power ON all child devices", "always_all"), + ConfigValueOption("Always power ON sync leader", "always_leader"), + ConfigValueOption("Start with powered players", "powered_only"), + ConfigValueOption("Ignore", "ignore"), + ), + description="What should happen if you power ON a sync group " + "(or you start playback to it), while no (or not all) players " + "are powered ON ?\n\nShould Music Assistant power ON all players, or only the " + "sync leader, or should it ignore the command if no players are powered ON ?", + required=False, + ), ) - if not player_id.startswith(SYNCGROUP_PREFIX): + + return ( + *BASE_PLAYER_CONFIG_ENTRIES, # add default entries for announce feature - entries = ( - *entries, - CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, - CONF_ENTRY_ANNOUNCE_VOLUME, - CONF_ENTRY_ANNOUNCE_VOLUME_MIN, - CONF_ENTRY_ANNOUNCE_VOLUME_MAX, - ) - return entries + CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, + CONF_ENTRY_ANNOUNCE_VOLUME, + CONF_ENTRY_ANNOUNCE_VOLUME_MIN, + CONF_ENTRY_ANNOUNCE_VOLUME_MAX, + ) def on_player_config_changed(self, config: PlayerConfig, changed_keys: set[str]) -> None: """Call (by config manager) when the configuration of a player changes."""