[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.
This commit is contained in:
Fabio Manganiello 2024-04-17 02:49:31 +02:00
parent 4972c8bdcf
commit f99f6bdab9
Signed by: blacklight
GPG key ID: D90FBA7F76362774
2 changed files with 55 additions and 41 deletions

View file

@ -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)

View file

@ -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,
)