add config options for sync groups
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 28 Aug 2024 11:48:11 +0000 (13:48 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 28 Aug 2024 11:48:11 +0000 (13:48 +0200)
music_assistant/common/models/config_entries.py
music_assistant/constants.py
music_assistant/server/controllers/players.py
music_assistant/server/models/player_provider.py

index 2d8941d9eb1f99e47afbfd3322cf0ef3d4d0fd6f..d4826fd4e4938e991d2a1fce437db475a1039f07 100644 (file)
@@ -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,
+)
index 2a8418eef0ab766efd09a18026d9810275b8e098..3442884d4590b3f9286f06207f33ab5fb9e59554 100644 (file)
@@ -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"
index 0e8618016ca0c7cc41332bde6fc26a7575e7855b..6a62ef6779b6d7a2832925ad06a54c1e34e0bee0 100644 (file)
@@ -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)
index 48711dc8a0beca893176cfef9116af183fdbf96e..f16467526925c9b2f6aa375e3bc5d69599e8b339 100644 (file)
@@ -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."""