Add play action in progress signal
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 25 Feb 2026 13:24:46 +0000 (14:24 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 25 Feb 2026 13:24:46 +0000 (14:24 +0100)
To inform frontend that a play media action is processing

music_assistant/constants.py
music_assistant/controllers/player_queues.py

index 351cbd2bc06c3bca1c0f1fef94565a30efcd3051..6c3bf3a85a993984b1935b5e0a364319c5c313f5 100644 (file)
@@ -990,6 +990,7 @@ ATTR_SUPPORTED_FEATURES: Final[str] = "supported_features"
 ATTR_MUTE_CONTROL: Final[str] = "mute_control"
 ATTR_VOLUME_CONTROL: Final[str] = "volume_control"
 ATTR_POWER_CONTROL: Final[str] = "power_control"
+ATTR_PLAY_ACTION_IN_PROGRESS: Final[str] = "play_action_in_progress"
 
 # Album type detection patterns
 LIVE_INDICATORS = [
index c5d00a02173d3e785dd5e7a2ba08fbefe90faa5a..b0209d6ddf65e23bb01bea64fe555979d9aacf22 100644 (file)
@@ -14,11 +14,13 @@ but it can also be something else, hence the loose coupling.
 from __future__ import annotations
 
 import asyncio
+import functools
 import random
 import time
+from collections.abc import Awaitable, Callable, Coroutine
 from contextlib import suppress
 from types import NoneType
-from typing import TYPE_CHECKING, Any, TypedDict, cast
+from typing import TYPE_CHECKING, Any, Concatenate, TypedDict, cast
 
 import shortuuid
 from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType
@@ -65,6 +67,7 @@ from music_assistant_models.queue_item import QueueItem
 from music_assistant.constants import (
     ATTR_ACTIVE_PLAYLIST,
     ATTR_ANNOUNCEMENT_IN_PROGRESS,
+    ATTR_PLAY_ACTION_IN_PROGRESS,
     MASS_LOGO_ONLINE,
     PLAYBACK_REPORT_INTERVAL_SECONDS,
     PLAYLIST_MEDIA_TYPES,
@@ -111,6 +114,41 @@ CACHE_CATEGORY_PLAYER_QUEUE_STATE = 0
 CACHE_CATEGORY_PLAYER_QUEUE_ITEMS = 1
 
 
+def handle_play_action[PlayerQueuesControllerT: "PlayerQueuesController", **P, R](
+    func: Callable[Concatenate[PlayerQueuesControllerT, P], Awaitable[R]],
+) -> Callable[Concatenate[PlayerQueuesControllerT, P], Coroutine[Any, Any, R]]:
+    """
+    Decorator to mark a play action in progress on the queue.
+
+    Fetches the queue based on queue_id, sets ATTR_PLAY_ACTION_IN_PROGRESS
+    to True before calling the function, and removes it after the function completes.
+
+    :param func: The function to wrap.
+    """  # noqa: D401
+
+    @functools.wraps(func)
+    async def wrapper(self: PlayerQueuesControllerT, *args: P.args, **kwargs: P.kwargs) -> R:
+        """Execute function with play action flag set."""
+        queue_id = kwargs.get("queue_id") or args[0]
+        assert isinstance(queue_id, str)  # for type checking
+        queue = self._queues.get(queue_id)
+        if queue is None:
+            # Queue not found, just call the function and let it handle the error
+            return await func(self, *args, **kwargs)
+        try:
+            has_flag = bool(queue.extra_attributes.get(ATTR_PLAY_ACTION_IN_PROGRESS))
+            if not has_flag:
+                queue.extra_attributes[ATTR_PLAY_ACTION_IN_PROGRESS] = True
+                self.signal_update(queue_id)
+            return await func(self, *args, **kwargs)
+        finally:
+            if not has_flag:
+                queue.extra_attributes.pop(ATTR_PLAY_ACTION_IN_PROGRESS, None)
+                self.signal_update(queue_id)
+
+    return wrapper
+
+
 class CompareState(TypedDict):
     """Simple object where we store the (previous) state of a queue.
 
@@ -424,6 +462,7 @@ class PlayerQueuesController(CoreController):
             await self.resume(queue_id)
 
     @api_command("player_queues/play_media")
+    @handle_play_action
     async def play_media(
         self,
         queue_id: str,
@@ -981,6 +1020,7 @@ class PlayerQueuesController(CoreController):
             raise QueueEmpty(msg)
 
     @api_command("player_queues/play_index")
+    @handle_play_action
     async def play_index(
         self,
         queue_id: str,