Player controller mypy fixes (#2546)
authorOzGav <gavnosp@hotmail.com>
Mon, 27 Oct 2025 23:11:00 +0000 (09:11 +1000)
committerGitHub <noreply@github.com>
Mon, 27 Oct 2025 23:11:00 +0000 (00:11 +0100)
music_assistant/controllers/players/player_controller.py
pyproject.toml

index 4f5d023878cf0c817006e783a5358a6ddb6dbe86..4f3fbe87ac981d4add1d8e187afcbb55a840dd1b 100644 (file)
@@ -19,6 +19,7 @@ from __future__ import annotations
 import asyncio
 import functools
 import time
+from collections.abc import Awaitable, Callable, Coroutine
 from contextlib import suppress
 from typing import TYPE_CHECKING, Any, Concatenate, TypedDict, cast
 
@@ -81,7 +82,7 @@ from music_assistant.models.plugin import PluginProvider, PluginSource
 from .sync_groups import SyncGroupController, SyncGroupPlayer
 
 if TYPE_CHECKING:
-    from collections.abc import Awaitable, Callable, Coroutine, Iterator
+    from collections.abc import Iterator
 
     from music_assistant_models.config_entries import CoreConfig, PlayerConfig
     from music_assistant_models.player_queue import PlayerQueue
@@ -103,9 +104,10 @@ def handle_player_command[PlayerControllerT: "PlayerController", **P, R](
     """Check and log commands to players."""
 
     @functools.wraps(func)
-    async def wrapper(self: PlayerControllerT, *args: P.args, **kwargs: P.kwargs) -> R | None:
+    async def wrapper(self: PlayerControllerT, *args: P.args, **kwargs: P.kwargs) -> None:
         """Log and handle_player_command commands to players."""
-        player_id = kwargs["player_id"] if "player_id" in kwargs else args[0]
+        player_id = kwargs.get("player_id") or args[0]
+        assert isinstance(player_id, str)  # for type checking
         if (player := self._players.get(player_id)) is None or not player.available:
             # player not existent
             self.logger.warning(
@@ -133,7 +135,7 @@ class PlayerController(CoreController):
 
     domain: str = "players"
 
-    def __init__(self, *args, **kwargs) -> None:
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
         """Initialize core controller."""
         super().__init__(*args, **kwargs)
         self._players: dict[str, Player] = {}
@@ -143,7 +145,7 @@ class PlayerController(CoreController):
             "Music Assistant's core controller which manages all players from all providers."
         )
         self.manifest.icon = "speaker-multiple"
-        self._poll_task: asyncio.Task | None = None
+        self._poll_task: asyncio.Task[None] | None = None
         self._player_throttlers: dict[str, Throttler] = {}
         self._announce_locks: dict[str, asyncio.Lock] = {}
         self._sync_groups: SyncGroupController = SyncGroupController(self)
@@ -170,7 +172,7 @@ class PlayerController(CoreController):
     @property
     def providers(self) -> list[PlayerProvider]:
         """Return all loaded/running MusicProviders."""
-        return self.mass.get_providers(ProviderType.PLAYER)  # type: ignore=return-value
+        return cast("list[PlayerProvider]", self.mass.get_providers(ProviderType.PLAYER))
 
     def all(
         self,
@@ -962,7 +964,7 @@ class PlayerController(CoreController):
                 # we can send a regular play-media call downstream
                 announce_data = AnnounceData(
                     announcement_url=url,
-                    pre_announce=pre_announce,
+                    pre_announce=bool(pre_announce or False),
                     pre_announce_url=pre_announce_url,
                 )
                 announcement = PlayerMedia(
@@ -971,7 +973,7 @@ class PlayerController(CoreController):
                     ),
                     media_type=MediaType.ANNOUNCEMENT,
                     title="Announcement",
-                    custom_data=announce_data,
+                    custom_data=dict(announce_data),
                 )
                 # handle native announce support
                 if native_announce_support:
@@ -1020,7 +1022,7 @@ class PlayerController(CoreController):
         # check if source is a pluginsource
         # in that case the source id is the instance_id of the plugin provider
         if plugin_prov := self.mass.get_provider(source):
-            await self._handle_select_plugin_source(player, plugin_prov)
+            await self._handle_select_plugin_source(player, cast("PluginProvider", plugin_prov))
             return
         # check if source is a mass queue
         # this can be used to restore the queue after a source switch
@@ -1266,7 +1268,7 @@ class PlayerController(CoreController):
     @api_command("players/create_group_player")
     async def create_group_player(
         self, provider: str, name: str, members: list[str], dynamic: bool = True
-    ):
+    ) -> Player:
         """
         Create a new (permanent) Group Player.
 
@@ -1707,28 +1709,33 @@ class PlayerController(CoreController):
             return None
         volume_level = volume_override
         if volume_level is None and volume_strategy == "absolute":
-            volume_level = volume_strategy_volume
+            volume_level = int(cast("float", volume_strategy_volume))
         elif volume_level is None and volume_strategy == "relative":
-            player = self.get(player_id)
-            volume_level = player.volume_level + volume_strategy_volume
+            if (player := self.get(player_id)) and player.volume_level is not None:
+                volume_level = int(player.volume_level + cast("float", volume_strategy_volume))
         elif volume_level is None and volume_strategy == "percentual":
-            player = self.get(player_id)
-            percentual = (player.volume_level / 100) * volume_strategy_volume
-            volume_level = player.volume_level + percentual
+            if (player := self.get(player_id)) and player.volume_level is not None:
+                percentual = (player.volume_level / 100) * cast("float", volume_strategy_volume)
+                volume_level = int(player.volume_level + percentual)
         if volume_level is not None:
-            announce_volume_min = self.mass.config.get_raw_player_config_value(
-                player_id,
-                CONF_ENTRY_ANNOUNCE_VOLUME_MIN.key,
-                CONF_ENTRY_ANNOUNCE_VOLUME_MIN.default_value,
+            announce_volume_min = cast(
+                "float",
+                self.mass.config.get_raw_player_config_value(
+                    player_id,
+                    CONF_ENTRY_ANNOUNCE_VOLUME_MIN.key,
+                    CONF_ENTRY_ANNOUNCE_VOLUME_MIN.default_value,
+                ),
             )
-            volume_level = max(announce_volume_min, volume_level)
-            announce_volume_max = self.mass.config.get_raw_player_config_value(
-                player_id,
-                CONF_ENTRY_ANNOUNCE_VOLUME_MAX.key,
-                CONF_ENTRY_ANNOUNCE_VOLUME_MAX.default_value,
+            volume_level = max(int(announce_volume_min), volume_level)
+            announce_volume_max = cast(
+                "float",
+                self.mass.config.get_raw_player_config_value(
+                    player_id,
+                    CONF_ENTRY_ANNOUNCE_VOLUME_MAX.key,
+                    CONF_ENTRY_ANNOUNCE_VOLUME_MAX.default_value,
+                ),
             )
-            volume_level = min(announce_volume_max, volume_level)
-        # ensure the result is an integer
+            volume_level = min(int(announce_volume_max), volume_level)
         return None if volume_level is None else int(volume_level)
 
     def iter_group_members(
@@ -2026,15 +2033,21 @@ class PlayerController(CoreController):
         await self.wait_for_state(player, PlaybackState.PLAYING, 10, minimal_time=0.1)
         # wait for the player to stop playing
         if not announcement.duration:
+            if not announcement.custom_data:
+                raise ValueError("Announcement missing duration and custom_data")
             media_info = await async_parse_tags(
                 announcement.custom_data["announcement_url"], require_duration=True
             )
-            announcement.duration = media_info.duration
+            announcement.duration = int(media_info.duration) if media_info.duration else None
+
+        if announcement.duration is None:
+            raise ValueError("Announcement duration could not be determined")
+
         await self.wait_for_state(
             player,
             PlaybackState.IDLE,
             timeout=announcement.duration + 6,
-            minimal_time=announcement.duration,
+            minimal_time=float(announcement.duration),
         )
         self.logger.debug(
             "Announcement to player %s - restore previous state...", player.display_name
@@ -2093,7 +2106,7 @@ class PlayerController(CoreController):
                     self.mass.loop.call_soon(
                         self.mass.player_queues.on_player_update,
                         player,
-                        {"corrected_elapsed_time": player.corrected_elapsed_time},
+                        {"corrected_elapsed_time": (None, player.corrected_elapsed_time)},
                     )
                 # Poll player;
                 if not player.needs_poll:
index 5c4907c1a18b78fbe843622f9684246cca049ca9..c90a77c4afa37eaf371c57dae02383b6aca310d6 100644 (file)
@@ -136,7 +136,6 @@ exclude = [
   '^music_assistant/controllers/metadata.py$',
   '^music_assistant/controllers/music.py$',
   '^music_assistant/controllers/player_queues.py$',
-  '^music_assistant/controllers/players/player_controller.py',
   '^music_assistant/controllers/streams.py$',
   '^music_assistant/controllers/webserver.py',
   '^music_assistant/helpers/app_vars.py',