adopt changes to pychromecast
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 4 Mar 2021 17:26:07 +0000 (18:26 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 4 Mar 2021 17:26:07 +0000 (18:26 +0100)
.vscode/settings.json
music_assistant/helpers/process.py
music_assistant/managers/players.py
music_assistant/providers/chromecast/__init__.py
music_assistant/providers/chromecast/models.py
music_assistant/providers/chromecast/player.py
requirements.txt

index 7e6073639bfa97738935be888a180404ff1514e8..889b7d543f56c959b0e70b8ace4fc749c06a4dbe 100644 (file)
@@ -2,7 +2,7 @@
     "python.linting.pylintEnabled": true,
     "python.linting.pylintArgs": ["--rcfile=${workspaceFolder}/setup.cfg"],
     "python.linting.enabled": true,
-    "python.pythonPath": "/Users/marcelvanderveldt/Workdir/music-assistant/server/.venv/bin/python3",
+    "python.pythonPath": "/Users/marcelvanderveldt/Workdir/music-assistant/server/.venv/bin/python3.9",
     "python.linting.flake8Enabled": true,
     "python.linting.flake8Args": ["--config=${workspaceFolder}/setup.cfg"],
     "python.linting.mypyEnabled": false,
index 661de90ed15789d575a8e6f8cc2ffc1498b1e3bb..853be329184cf0d4c7c9f12bda6360c1d305c336 100644 (file)
@@ -9,9 +9,12 @@ import asyncio
 import logging
 from typing import AsyncGenerator, List, Optional
 
+from async_timeout import timeout
+
 LOGGER = logging.getLogger("AsyncProcess")
 
 DEFAULT_CHUNKSIZE = 512000
+DEFAULT_TIMEOUT = 5
 
 
 class AsyncProcess:
@@ -55,10 +58,13 @@ class AsyncProcess:
             if len(chunk) < chunk_size:
                 break
 
-    async def read(self, chunk_size: int = DEFAULT_CHUNKSIZE) -> bytes:
+    async def read(
+        self, chunk_size: int = DEFAULT_CHUNKSIZE, time_out: int = DEFAULT_TIMEOUT
+    ) -> bytes:
         """Read x bytes from the process stdout."""
         try:
-            return await self._proc.stdout.readexactly(chunk_size)
+            async with timeout(time_out):
+                return await self._proc.stdout.readexactly(chunk_size)
         except asyncio.IncompleteReadError as err:
             return err.partial
 
index f3765efbaa50e499ce150611918c2958cfcdf420..2bd93d6a449f9c311d76dc6ad2dce3811d9ae490 100755 (executable)
@@ -1,5 +1,6 @@
 """PlayerManager: Orchestrates all players from player providers."""
 
+import asyncio
 import logging
 from typing import Dict, List, Optional, Set, Tuple, Union
 
@@ -10,7 +11,7 @@ from music_assistant.constants import (
     EVENT_PLAYER_REMOVED,
 )
 from music_assistant.helpers.typing import MusicAssistant
-from music_assistant.helpers.util import callback, run_periodic, try_parse_int
+from music_assistant.helpers.util import callback, try_parse_int
 from music_assistant.helpers.web import api_route
 from music_assistant.models.media_types import MediaItem, MediaType
 from music_assistant.models.player import (
@@ -49,15 +50,22 @@ class PlayerManager:
         for player in self:
             await player.on_remove()
 
-    @run_periodic(30)
     async def poll_task(self):
         """Check for updates on players that need to be polled."""
-        for player in self:
-            if not player.player_state.available:
-                continue
-            if not player.should_poll:
-                continue
-            await player.on_poll()
+        count = 0
+        while True:
+            for player in self:
+                if not player.player_state.available:
+                    continue
+                if not player.should_poll:
+                    continue
+                if player.state == PlaybackState.Playing or count == POLL_INTERVAL:
+                    await player.on_poll()
+            if count == POLL_INTERVAL:
+                count = 0
+            else:
+                count += 1
+            await asyncio.sleep(1)
 
     @property
     def players(self) -> Dict[str, Player]:
index 49c9ec876c3e905119ed4941bc18773d9167aca5..f1f5939c28ec6658c26fc95dea8bfbb03facfdcf 100644 (file)
@@ -85,6 +85,7 @@ class ChromecastProvider(PlayerProvider):
             host=service[4],
             port=service[5],
         )
+        cast_info.fill_out_missing_chromecast_info(self.mass.zeroconf)
         player_id = cast_info.uuid
         LOGGER.debug(
             "Chromecast discovered: %s (%s)", cast_info.friendly_name, player_id
index 1664757eff76d6aa9c9fb2b1d15cc14c03407ea8..2c08064f52b30b3d98f127fa8f04e04ab6af1885 100644 (file)
@@ -7,6 +7,7 @@ import logging
 from dataclasses import dataclass, field
 from typing import Optional, Tuple
 
+from pychromecast import dial
 from pychromecast.const import CAST_MANUFACTURERS
 
 from .const import PROV_ID
@@ -23,11 +24,14 @@ class ChromecastInfo:
     """
 
     services: Optional[set] = field(default_factory=set)
+    uuid: Optional[str] = None
+    model_name: str = ""
+    friendly_name: Optional[str] = None
+    is_dynamic_group: bool = False
+    manufacturer: Optional[str] = None
     host: Optional[str] = ""
     port: Optional[int] = 0
-    uuid: Optional[str] = ""
-    model_name: str = ""
-    friendly_name: Optional[str] = ""
+    _info_requested: bool = field(init=False, default=False)
 
     def __post_init__(self):
         """Convert UUID to string."""
@@ -43,12 +47,46 @@ class ChromecastInfo:
         """Return the host+port tuple."""
         return self.host, self.port
 
-    @property
-    def manufacturer(self) -> str:
-        """Return the manufacturer."""
-        if not self.model_name:
-            return None
-        return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.")
+    def fill_out_missing_chromecast_info(self, zconf) -> None:
+        """Lookup missing info for the Chromecast player."""
+        http_device_status = None
+
+        if self._info_requested:
+            return
+
+        # Fill out missing group information via HTTP API.
+        if self.is_audio_group:
+            http_group_status = None
+            if self.uuid:
+                http_group_status = dial.get_multizone_status(
+                    self.host,
+                    services=self.services,
+                    zconf=zconf,
+                )
+                if http_group_status is not None:
+                    self.is_dynamic_group = any(
+                        str(g.uuid) == self.uuid
+                        for g in http_group_status.dynamic_groups
+                    )
+        else:
+            # Fill out some missing information (friendly_name, uuid) via HTTP dial.
+            http_device_status = dial.get_device_status(
+                self.host, services=self.services, zconf=zconf
+            )
+        if not self.uuid and http_device_status:
+            self.uuid = http_device_status.uuid
+        if not self.friendly_name and http_device_status:
+            self.friendly_name = http_device_status.friendly_name
+        if not self.model_name and http_device_status:
+            self.model_name = http_device_status.model_name
+        if not self.manufacturer and http_device_status:
+            self.manufacturer = http_device_status.manufacturer
+        if not self.manufacturer and self.model_name:
+            self.manufacturer = CAST_MANUFACTURERS.get(
+                self.model_name.lower(), "Google Inc."
+            )
+
+        self._info_requested = True
 
 
 class CastStatusListener:
index 10ca0fffceb02a12df16075a61d6e67798061d0f..ee1ea31d75faf5806c147080bbc9863190e1f7ed 100644 (file)
@@ -76,6 +76,8 @@ class ChromecastPlayer(Player):
     @property
     def powered(self) -> bool:
         """Return power state of this player."""
+        if self.is_group_player:
+            return self._chromecast.is_idle
         return not self.cast_status.volume_muted if self.cast_status else False
 
     @property
@@ -172,8 +174,8 @@ class ChromecastPlayer(Player):
         """Call when player is added to the player manager."""
         chromecast = await self.mass.loop.run_in_executor(
             None,
-            pychromecast.get_chromecast_from_service,
-            (
+            pychromecast.get_chromecast_from_cast_info,
+            pychromecast.discovery.CastInfo(
                 self.services,
                 self._cast_info.uuid,
                 self._cast_info.model_name,
@@ -193,7 +195,7 @@ class ChromecastPlayer(Player):
         mz_controller = MultizoneController(chromecast.uuid)
         chromecast.register_handler(mz_controller)
         chromecast.mz_controller = mz_controller
-        self._chromecast.start()
+        chromecast.start()
 
     def set_cast_info(self, cast_info: ChromecastInfo) -> None:
         """Set (or update) the cast discovery info."""
index 35a90c9eb63fa830907172c47e91a83fabb897e8..254e66c7bb8e68e371be4d19d9cd9a324ffbf06f 100644 (file)
@@ -1,4 +1,5 @@
 argparse==1.4.0
+async-timeout==3.0.1
 pychromecast==9.1.1
 aiohttp[speedups]==3.7.4
 asyncio-throttle==1.0.1