Auto translate commands directed at protocol player id to visible parent
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 22 Feb 2026 23:10:43 +0000 (00:10 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 22 Feb 2026 23:10:43 +0000 (00:10 +0100)
music_assistant/controllers/players/controller.py
music_assistant/controllers/players/helpers.py

index 0192bdae7247d06d9a167fba64c67270791e1351..9b8162feb5e8d1fc2c9105929e84241c681cbd56 100644 (file)
@@ -486,6 +486,7 @@ class PlayerController(ProtocolLinkingMixin, CoreController):
         await self._handle_cmd_resume(player_id, source, media)
 
     @api_command("players/cmd/seek")
+    @handle_player_command
     async def cmd_seek(self, player_id: str, position: int) -> None:
         """Handle SEEK command for given player.
 
@@ -517,6 +518,7 @@ class PlayerController(ProtocolLinkingMixin, CoreController):
         await player.seek(position)
 
     @api_command("players/cmd/next")
+    @handle_player_command
     async def cmd_next_track(self, player_id: str) -> None:
         """Handle NEXT TRACK command for given player."""
         player = self._get_player_with_redirect(player_id)
@@ -545,6 +547,7 @@ class PlayerController(ProtocolLinkingMixin, CoreController):
         raise UnsupportedFeaturedException(msg)
 
     @api_command("players/cmd/previous")
+    @handle_player_command
     async def cmd_previous_track(self, player_id: str) -> None:
         """Handle PREVIOUS TRACK command for given player."""
         player = self._get_player_with_redirect(player_id)
@@ -2203,7 +2206,7 @@ class PlayerController(ProtocolLinkingMixin, CoreController):
 
         # Confirmed external source takeover
         self.logger.info(
-            "External source '%s' took over on %s while grouped via protocol %s - "
+            "External source '%s' took over on %s while playing via protocol %s - "
             "clearing active output protocol and ungrouping",
             new_source,
             player.display_name,
@@ -2581,7 +2584,7 @@ class PlayerController(ProtocolLinkingMixin, CoreController):
             return  # nothing to do
 
         # ungroup player at power off
-        player_was_synced = player.state.synced_to is not None
+        player_was_synced = bool(player.state.synced_to or player.group_members)
         if player.type == PlayerType.PLAYER and not powered:
             # ungroup player if it is synced (or is a sync leader itself)
             # NOTE: ungroup will be ignored if the player is not grouped or synced
index 0fa3c20967183570fd062c9a0793bebd8805b49a..beb9183d0b58d22e2cbdc87ecf916a7c0824029b 100644 (file)
@@ -82,6 +82,23 @@ def handle_player_command[PlayerControllerT: "PlayerController", **P, R](
                 )
                 return
 
+            # this should not happen, but in case a player_id of a protocol player is used,
+            # auto-resolve it to the parent player
+            if player.protocol_parent_id and (
+                protocol_parent := self._players.get(player.protocol_parent_id)
+            ):
+                player = protocol_parent
+                if "player_id" in kwargs:
+                    kwargs["player_id"] = protocol_parent.player_id
+                else:
+                    args = (protocol_parent.player_id, *args[1:])  # type: ignore[assignment]
+                self.logger.info(
+                    "Auto-resolved protocol player %s to linked parent %s for command %s",
+                    player_id,
+                    protocol_parent.player_id,
+                    fn.__name__,
+                )
+
             current_user = get_current_user()
             if (
                 current_user
@@ -101,7 +118,7 @@ def handle_player_command[PlayerControllerT: "PlayerController", **P, R](
             )
 
             async def execute() -> None:
-                async with self._player_throttlers[player_id]:
+                async with self._player_throttlers[player.player_id]:
                     try:
                         await fn(self, *args, **kwargs)
                     except Exception as err: