From f99f6bdab959fae20606fcb2dc45aa2240e78e69 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 17 Apr 2024 02:49:31 +0200 Subject: [PATCH] [media.chromecast] Resource clean up + new API adaptations. - `pychromecast.get_chromecasts` returns both a list of devices and a browser object. Since the Chromecast plugin is the most likely culprit of the excessive number of open MDNS sockets, it seems that we may need to explicitly stop discovery on the browser and close the ZeroConf object after the discovery is done. - I was still using an ancient version of pychromecast on my RPi4, and I didn't notice that more recent versions implemented several breaking changes. Adapted the code to cope with those changes. --- .../plugins/media/chromecast/__init__.py | 80 ++++++++++--------- .../plugins/media/chromecast/_subtitles.py | 16 +++- 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/platypush/plugins/media/chromecast/__init__.py b/platypush/plugins/media/chromecast/__init__.py index 9afb4357..fcf1a446 100644 --- a/platypush/plugins/media/chromecast/__init__.py +++ b/platypush/plugins/media/chromecast/__init__.py @@ -31,7 +31,6 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): :param poll_interval: How often the plugin should poll for new/removed Chromecast devices (default: 30 seconds). """ - super().__init__(poll_interval=poll_interval, **kwargs) self._is_local = False @@ -42,11 +41,18 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): def _get_chromecasts(self, *args, **kwargs): with self._refresh_lock: - chromecasts = pychromecast.get_chromecasts(*args, **kwargs) + ret = pychromecast.get_chromecasts(*args, **kwargs) - if isinstance(chromecasts, tuple): - return chromecasts[0] - return chromecasts + if isinstance(ret, tuple): + chromecasts, browser = ret + if browser: + browser.stop_discovery() + if browser.zc: + browser.zc.close() + + return chromecasts + + return ret @staticmethod def _get_device_property(cc, prop: str): @@ -58,14 +64,25 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): """ Convert a Chromecast object and its status to a dictionary. """ + if hasattr(cc, 'cast_info'): # Newer PyChromecast API + host = cc.cast_info.host + port = cc.cast_info.port + elif hasattr(cc, 'host'): + host = getattr(cc, 'host', None) + port = getattr(cc, 'port', None) + elif hasattr(cc, 'uri'): + host, port = cc.uri.split(':') + else: + raise RuntimeError('Invalid Chromecast object') + return { 'type': cc.cast_type, 'name': cc.name, 'manufacturer': self._get_device_property(cc, 'manufacturer'), 'model_name': cc.model_name, 'uuid': str(cc.uuid), - 'address': cc.host if hasattr(cc, 'host') else cc.uri.split(':')[0], - 'port': cc.port if hasattr(cc, 'port') else int(cc.uri.split(':')[1]), + 'address': host, + 'port': port, 'status': ( { 'app': { @@ -284,24 +301,23 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): chromecast = chromecast or self.chromecast cast = self.get_chromecast(chromecast) - if cast.media_controller.is_paused: + if cast.media_controller.status.player_is_paused: cast.media_controller.play() - elif cast.media_controller.is_playing: + elif cast.media_controller.status.player_is_playing: cast.media_controller.pause() cast.wait() return self.status(chromecast=chromecast) @action - def stop(self, *_, chromecast: Optional[str] = None, **__): + def stop(self, *_, chromecast: Optional[str] = None, **__): # type: ignore chromecast = chromecast or self.chromecast if not chromecast: - return + return None cast = self.get_chromecast(chromecast) cast.media_controller.stop() cast.wait() - return self.status(chromecast=chromecast) @action @@ -347,51 +363,51 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): def is_playing(self, chromecast: Optional[str] = None, **_): return self.get_chromecast( chromecast or self.chromecast - ).media_controller.is_playing + ).media_controller.status.player_is_playing @action def is_paused(self, chromecast: Optional[str] = None, **_): return self.get_chromecast( chromecast or self.chromecast - ).media_controller.is_paused + ).media_controller.status.player_is_paused @action def is_idle(self, chromecast: Optional[str] = None): return self.get_chromecast( chromecast or self.chromecast - ).media_controller.is_idle + ).media_controller.status.player_is_idle @action def list_subtitles(self, chromecast: Optional[str] = None): return self.get_chromecast( chromecast or self.chromecast - ).media_controller.subtitle_tracks + ).media_controller.status.subtitle_tracks @action def enable_subtitles( - self, chromecast: Optional[str] = None, track_id: Optional[str] = None, **_ + self, chromecast: Optional[str] = None, track_id: Optional[int] = None, **_ ): mc = self.get_chromecast(chromecast or self.chromecast).media_controller if track_id is not None: return mc.enable_subtitle(track_id) - if mc.subtitle_tracks: - return mc.enable_subtitle(mc.subtitle_tracks[0].get('trackId')) + if mc.status.subtitle_tracks: + return mc.enable_subtitle(mc.status.subtitle_tracks[0].get('trackId')) @action def disable_subtitles( - self, chromecast: Optional[str] = None, track_id: Optional[str] = None, **_ + self, chromecast: Optional[str] = None, track_id: Optional[int] = None, **_ ): mc = self.get_chromecast(chromecast or self.chromecast).media_controller if track_id: return mc.disable_subtitle(track_id) - if mc.current_subtitle_tracks: - return mc.disable_subtitle(mc.current_subtitle_tracks[0]) + if mc.status.current_subtitle_tracks: + return mc.disable_subtitle(mc.status.current_subtitle_tracks[0]) @action def toggle_subtitles(self, chromecast: Optional[str] = None, **_): mc = self.get_chromecast(chromecast or self.chromecast).media_controller all_subs = mc.status.subtitle_tracks - cur_subs = mc.status.status.current_subtitle_tracks + cur_subs = mc.status.current_subtitle_tracks if cur_subs: return self.disable_subtitles(chromecast, cur_subs[0]) @@ -511,7 +527,6 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): self, chromecast: Optional[str] = None, timeout: Optional[float] = None, - blocking: bool = True, ): """ Disconnect a Chromecast and wait for it to terminate @@ -520,11 +535,9 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): the default configured Chromecast will be used. :param timeout: Number of seconds to wait for disconnection (default: None: block until termination). - :param blocking: If set (default), then the code will wait until - disconnection, otherwise it will return immediately. """ cast = self.get_chromecast(chromecast) - cast.disconnect(timeout=timeout, blocking=blocking) + cast.disconnect(timeout=timeout) @action def join(self, chromecast: Optional[str] = None, timeout: Optional[float] = None): @@ -550,17 +563,6 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): cast = self.get_chromecast(chromecast) cast.quit_app() - @action - def reboot(self, chromecast: Optional[str] = None): - """ - Reboots the Chromecast - - :param chromecast: Chromecast to cast to. If none is specified, then - the default configured Chromecast will be used. - """ - cast = self.get_chromecast(chromecast) - cast.reboot() - @action def set_volume(self, volume: float, chromecast: Optional[str] = None): """ @@ -621,7 +623,7 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin): """ chromecast = chromecast or self.chromecast cast = self.get_chromecast(chromecast) - cast.set_volume_muted(not cast.status.volume_muted) + cast.set_volume_muted(not cast.media_controller.status.volume_muted) cast.wait() return self.status(chromecast=chromecast) diff --git a/platypush/plugins/media/chromecast/_subtitles.py b/platypush/plugins/media/chromecast/_subtitles.py index 450e7a26..ca3b2c49 100644 --- a/platypush/plugins/media/chromecast/_subtitles.py +++ b/platypush/plugins/media/chromecast/_subtitles.py @@ -1,5 +1,9 @@ -# pylint: disable=too-few-public-methods -class SubtitlesAsyncHandler: +import logging + +from pychromecast.controllers.media import MediaStatusListener + + +class SubtitlesAsyncHandler(MediaStatusListener): """ This class is used to enable subtitles when the media is loaded. """ @@ -8,9 +12,17 @@ class SubtitlesAsyncHandler: self.mc = mc self.subtitle_id = subtitle_id self.initialized = False + self.logger = logging.getLogger(__name__) def new_media_status(self, *_): if self.subtitle_id and not self.initialized: self.mc.update_status() self.mc.enable_subtitle(self.subtitle_id) self.initialized = True + + def load_media_failed(self, queue_item_id: int, error_code: int) -> None: + self.logger.warning( + "Failed to load media with queue_item_id %d, error code: %d", + queue_item_id, + error_code, + )