From: Marcel van der Veldt Date: Thu, 4 Mar 2021 17:26:07 +0000 (+0100) Subject: adopt changes to pychromecast X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=2c07748f1503d92d3f1b11619a83e9d95a694a67;p=music-assistant-server.git adopt changes to pychromecast --- diff --git a/.vscode/settings.json b/.vscode/settings.json index 7e607363..889b7d54 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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, diff --git a/music_assistant/helpers/process.py b/music_assistant/helpers/process.py index 661de90e..853be329 100644 --- a/music_assistant/helpers/process.py +++ b/music_assistant/helpers/process.py @@ -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 diff --git a/music_assistant/managers/players.py b/music_assistant/managers/players.py index f3765efb..2bd93d6a 100755 --- a/music_assistant/managers/players.py +++ b/music_assistant/managers/players.py @@ -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]: diff --git a/music_assistant/providers/chromecast/__init__.py b/music_assistant/providers/chromecast/__init__.py index 49c9ec87..f1f5939c 100644 --- a/music_assistant/providers/chromecast/__init__.py +++ b/music_assistant/providers/chromecast/__init__.py @@ -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 diff --git a/music_assistant/providers/chromecast/models.py b/music_assistant/providers/chromecast/models.py index 1664757e..2c08064f 100644 --- a/music_assistant/providers/chromecast/models.py +++ b/music_assistant/providers/chromecast/models.py @@ -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: diff --git a/music_assistant/providers/chromecast/player.py b/music_assistant/providers/chromecast/player.py index 10ca0fff..ee1ea31d 100644 --- a/music_assistant/providers/chromecast/player.py +++ b/music_assistant/providers/chromecast/player.py @@ -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.""" diff --git a/requirements.txt b/requirements.txt index 35a90c9e..254e66c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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