2023-11-12 03:08:54 +01:00
|
|
|
import threading
|
|
|
|
from typing import Callable, Optional
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
import pychromecast # type: ignore
|
|
|
|
|
|
|
|
from platypush.backend.http.app.utils import get_remote_base_url
|
|
|
|
from platypush.plugins import RunnablePlugin, action
|
2019-02-05 02:30:20 +01:00
|
|
|
from platypush.plugins.media import MediaPlugin
|
2019-02-06 11:51:44 +01:00
|
|
|
from platypush.utils import get_mime_type
|
2023-11-12 03:08:54 +01:00
|
|
|
from platypush.message.event.media import MediaPlayRequestEvent
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
from ._listener import MediaListener
|
|
|
|
from ._subtitles import SubtitlesAsyncHandler
|
|
|
|
from ._utils import convert_status, post_event
|
2018-11-13 20:20:55 +01:00
|
|
|
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
|
|
|
|
"""
|
|
|
|
Plugin to control Chromecast devices.
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
STREAM_TYPE_UNKNOWN = "UNKNOWN"
|
|
|
|
STREAM_TYPE_BUFFERED = "BUFFERED"
|
|
|
|
STREAM_TYPE_LIVE = "LIVE"
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
def __init__(
|
|
|
|
self, chromecast: Optional[str] = None, poll_interval: float = 30, **kwargs
|
|
|
|
):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Default Chromecast to cast to if no name is specified.
|
|
|
|
:param poll_interval: How often the plugin should poll for new/removed
|
|
|
|
Chromecast devices (default: 30 seconds).
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
super().__init__(poll_interval=poll_interval, **kwargs)
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2019-02-05 02:30:20 +01:00
|
|
|
self._is_local = False
|
2018-11-12 16:50:20 +01:00
|
|
|
self.chromecast = chromecast
|
|
|
|
self.chromecasts = {}
|
2019-06-27 23:52:40 +02:00
|
|
|
self._media_listeners = {}
|
2023-11-12 03:08:54 +01:00
|
|
|
self._refresh_lock = threading.RLock()
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
def _get_chromecasts(self, *args, **kwargs):
|
|
|
|
with self._refresh_lock:
|
|
|
|
chromecasts = pychromecast.get_chromecasts(*args, **kwargs)
|
2023-09-24 16:54:43 +02:00
|
|
|
|
2020-07-25 01:35:06 +02:00
|
|
|
if isinstance(chromecasts, tuple):
|
|
|
|
return chromecasts[0]
|
|
|
|
return chromecasts
|
|
|
|
|
2021-12-11 22:14:47 +01:00
|
|
|
@staticmethod
|
|
|
|
def _get_device_property(cc, prop: str):
|
2023-09-24 16:54:43 +02:00
|
|
|
if hasattr(cc, 'device'): # Previous pychromecast API
|
2021-12-11 22:14:47 +01:00
|
|
|
return getattr(cc.device, prop)
|
|
|
|
return getattr(cc.cast_info, prop)
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
def _serialize_device(self, cc: pychromecast.Chromecast) -> dict:
|
|
|
|
"""
|
|
|
|
Convert a Chromecast object and its status to a dictionary.
|
|
|
|
"""
|
|
|
|
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]),
|
|
|
|
'status': (
|
|
|
|
{
|
|
|
|
'app': {
|
|
|
|
'id': cc.app_id,
|
|
|
|
'name': cc.app_display_name,
|
|
|
|
},
|
|
|
|
'is_active_input': cc.status.is_active_input,
|
|
|
|
'is_stand_by': cc.status.is_stand_by,
|
|
|
|
'is_idle': cc.is_idle,
|
|
|
|
'namespaces': cc.status.namespaces,
|
|
|
|
'volume': round(100 * cc.status.volume_level, 2),
|
|
|
|
'muted': cc.status.volume_muted,
|
|
|
|
**convert_status(cc.media_controller.status),
|
|
|
|
}
|
|
|
|
if cc.status
|
|
|
|
else {}
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
def _refresh_chromecasts(
|
|
|
|
self,
|
|
|
|
tries: int = 2,
|
|
|
|
retry_wait: float = 10,
|
|
|
|
timeout: float = 60,
|
|
|
|
blocking: bool = True,
|
|
|
|
callback: Optional[Callable] = None,
|
2023-09-24 16:54:43 +02:00
|
|
|
):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
|
|
|
Get the list of Chromecast devices
|
2019-06-18 18:14:24 +02:00
|
|
|
|
|
|
|
:param tries: Number of retries (default: 2)
|
|
|
|
:param retry_wait: Number of seconds between retries (default: 10 seconds)
|
|
|
|
:param timeout: Timeout before failing the call (default: 60 seconds)
|
2023-11-12 03:08:54 +01:00
|
|
|
:param blocking: If true, then the function will block until all the
|
|
|
|
Chromecast devices have been scanned. If false, then the provided
|
|
|
|
callback function will be invoked when a new device is discovered
|
|
|
|
:param callback: If blocking is false, then you can provide a callback
|
|
|
|
function that will be invoked when a new device is discovered
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2023-11-12 03:08:54 +01:00
|
|
|
self.chromecasts = {
|
|
|
|
self._get_device_property(cast, 'friendly_name'): cast
|
|
|
|
for cast in self._get_chromecasts(
|
|
|
|
tries=tries,
|
|
|
|
retry_wait=retry_wait,
|
|
|
|
timeout=timeout,
|
|
|
|
blocking=blocking,
|
|
|
|
callback=callback,
|
|
|
|
)
|
|
|
|
}
|
2019-01-20 15:16:16 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
for name, cast in self.chromecasts.items():
|
|
|
|
self._update_listeners(name, cast)
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
for cast in self.chromecasts.values():
|
|
|
|
cast.wait()
|
|
|
|
|
|
|
|
def _event_callback(self, _, cast: pychromecast.Chromecast):
|
|
|
|
with self._refresh_lock:
|
|
|
|
self.chromecasts[self._get_device_property(cast, 'friendly_name')] = cast
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
def _update_listeners(self, name, cast):
|
|
|
|
if name not in self._media_listeners:
|
|
|
|
cast.start()
|
2023-11-12 03:08:54 +01:00
|
|
|
self._media_listeners[name] = MediaListener(
|
|
|
|
name=name, cast=cast, callback=self._event_callback
|
|
|
|
)
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.media_controller.register_status_listener(self._media_listeners[name])
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2019-06-18 18:14:24 +02:00
|
|
|
def get_chromecast(self, chromecast=None, n_tries=2):
|
2019-02-05 02:30:20 +01:00
|
|
|
if isinstance(chromecast, pychromecast.Chromecast):
|
2023-11-12 03:08:54 +01:00
|
|
|
assert chromecast, 'Invalid Chromecast object'
|
2019-02-05 02:30:20 +01:00
|
|
|
return chromecast
|
|
|
|
|
2018-11-12 16:50:20 +01:00
|
|
|
if not chromecast:
|
|
|
|
if not self.chromecast:
|
2023-09-24 16:54:43 +02:00
|
|
|
raise RuntimeError(
|
|
|
|
'No Chromecast specified nor default Chromecast configured'
|
|
|
|
)
|
2018-11-12 16:50:20 +01:00
|
|
|
chromecast = self.chromecast
|
|
|
|
|
|
|
|
if chromecast not in self.chromecasts:
|
2019-02-05 02:30:20 +01:00
|
|
|
casts = {}
|
|
|
|
while n_tries > 0:
|
|
|
|
n_tries -= 1
|
2023-09-24 16:54:43 +02:00
|
|
|
casts.update(
|
|
|
|
{
|
|
|
|
self._get_device_property(cast, 'friendly_name'): cast
|
|
|
|
for cast in self._get_chromecasts()
|
|
|
|
}
|
|
|
|
)
|
2019-02-05 02:30:20 +01:00
|
|
|
|
|
|
|
if chromecast in casts:
|
|
|
|
self.chromecasts.update(casts)
|
|
|
|
break
|
2018-11-13 20:20:55 +01:00
|
|
|
|
|
|
|
if chromecast not in self.chromecasts:
|
2023-11-12 03:08:54 +01:00
|
|
|
raise RuntimeError(f'Device {chromecast} not found')
|
2018-11-13 20:20:55 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
cast = self.chromecasts[chromecast]
|
2023-11-12 15:52:31 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
cast.wait()
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.warning('Failed to wait Chromecast sync: %s', e)
|
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
return cast
|
2018-11-12 16:50:20 +01:00
|
|
|
|
|
|
|
@action
|
2023-09-24 16:54:43 +02:00
|
|
|
def play(
|
|
|
|
self,
|
2023-11-12 03:08:54 +01:00
|
|
|
resource: str,
|
|
|
|
*_,
|
|
|
|
content_type: Optional[str] = None,
|
|
|
|
chromecast: Optional[str] = None,
|
|
|
|
title: Optional[str] = None,
|
|
|
|
image_url: Optional[str] = None,
|
|
|
|
autoplay: bool = True,
|
|
|
|
current_time: int = 0,
|
|
|
|
stream_type: str = STREAM_TYPE_BUFFERED,
|
|
|
|
subtitles: Optional[str] = None,
|
|
|
|
subtitles_lang: str = 'en-US',
|
|
|
|
subtitles_mime: str = 'text/vtt',
|
|
|
|
subtitle_id: int = 1,
|
|
|
|
**__,
|
2023-09-24 16:54:43 +02:00
|
|
|
):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2023-11-12 03:08:54 +01:00
|
|
|
Cast media to an available Chromecast device.
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2019-02-05 02:30:20 +01:00
|
|
|
:param resource: Media to cast
|
|
|
|
:param content_type: Content type as a MIME type string
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
the default configured Chromecast will be used.
|
2018-11-12 16:50:20 +01:00
|
|
|
:param title: Optional title
|
|
|
|
:param image_url: URL of the image to use for the thumbnail
|
2023-11-12 03:08:54 +01:00
|
|
|
:param autoplay: Set it to false if you don't want the content to start
|
|
|
|
playing immediately (default: true)
|
2018-11-12 16:50:20 +01:00
|
|
|
:param current_time: Time to start the playback in seconds (default: 0)
|
2023-11-12 03:08:54 +01:00
|
|
|
:param stream_type: Type of stream to cast. Can be BUFFERED (default),
|
|
|
|
LIVE or UNKNOWN
|
2018-11-12 16:50:20 +01:00
|
|
|
:param subtitles: URL of the subtitles to be shown
|
|
|
|
:param subtitles_lang: Subtitles language (default: en-US)
|
|
|
|
:param subtitles_mime: Subtitles MIME type (default: text/vtt)
|
|
|
|
:param subtitle_id: ID of the subtitles to be loaded (default: 1)
|
|
|
|
"""
|
|
|
|
|
2018-11-13 20:20:55 +01:00
|
|
|
if not chromecast:
|
|
|
|
chromecast = self.chromecast
|
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
post_event(MediaPlayRequestEvent, resource=resource, device=chromecast)
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2018-11-12 16:50:20 +01:00
|
|
|
mc = cast.media_controller
|
2019-02-05 02:30:20 +01:00
|
|
|
resource = self._get_resource(resource)
|
2019-02-06 11:51:44 +01:00
|
|
|
|
2019-02-05 02:30:20 +01:00
|
|
|
if not content_type:
|
2019-02-06 11:51:44 +01:00
|
|
|
content_type = get_mime_type(resource)
|
2019-02-05 02:30:20 +01:00
|
|
|
|
2018-11-13 20:20:55 +01:00
|
|
|
if not content_type:
|
2023-11-12 03:08:54 +01:00
|
|
|
raise RuntimeError(f'content_type required to process media {resource}')
|
2018-11-13 20:20:55 +01:00
|
|
|
|
2023-09-24 16:54:43 +02:00
|
|
|
if not resource.startswith('http://') and not resource.startswith('https://'):
|
2023-11-12 03:08:54 +01:00
|
|
|
resource = self._start_streaming(resource)['url']
|
|
|
|
resource = get_remote_base_url() + resource
|
|
|
|
self.logger.info('HTTP media stream started on %s', resource)
|
|
|
|
|
|
|
|
if self._latest_resource:
|
|
|
|
if not title:
|
|
|
|
title = self._latest_resource.title
|
|
|
|
if not image_url:
|
|
|
|
image_url = self._latest_resource.image
|
2019-02-06 11:51:44 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
self.logger.info('Playing %s on %s', resource, chromecast)
|
2018-11-13 01:29:24 +01:00
|
|
|
|
2023-09-24 16:54:43 +02:00
|
|
|
mc.play_media(
|
|
|
|
resource,
|
|
|
|
content_type,
|
2023-11-12 03:08:54 +01:00
|
|
|
title=self._latest_resource.title if self._latest_resource else title,
|
2023-09-24 16:54:43 +02:00
|
|
|
thumb=image_url,
|
|
|
|
current_time=current_time,
|
|
|
|
autoplay=autoplay,
|
|
|
|
stream_type=stream_type,
|
|
|
|
subtitles=subtitles,
|
|
|
|
subtitles_lang=subtitles_lang,
|
|
|
|
subtitles_mime=subtitles_mime,
|
|
|
|
subtitle_id=subtitle_id,
|
|
|
|
)
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
if subtitles:
|
2023-11-12 03:08:54 +01:00
|
|
|
mc.register_status_listener(SubtitlesAsyncHandler(mc, subtitle_id))
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
mc.block_until_active()
|
2020-02-21 18:40:46 +01:00
|
|
|
if self.volume:
|
|
|
|
self.set_volume(volume=self.volume, chromecast=chromecast)
|
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-13 20:20:55 +01:00
|
|
|
|
2018-11-13 23:09:19 +01:00
|
|
|
@action
|
2019-02-05 02:30:20 +01:00
|
|
|
def load(self, *args, **kwargs):
|
2023-11-12 03:08:54 +01:00
|
|
|
"""
|
|
|
|
Alias for :meth:`.play`.
|
|
|
|
"""
|
2019-02-05 02:30:20 +01:00
|
|
|
return self.play(*args, **kwargs)
|
2018-11-13 23:09:19 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def pause(self, *_, chromecast: Optional[str] = None, **__):
|
|
|
|
"""
|
|
|
|
Pause the current media on the Chromecast.
|
|
|
|
"""
|
2019-06-27 23:52:40 +02:00
|
|
|
chromecast = chromecast or self.chromecast
|
|
|
|
cast = self.get_chromecast(chromecast)
|
|
|
|
|
2018-11-13 23:17:51 +01:00
|
|
|
if cast.media_controller.is_paused:
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.media_controller.play()
|
2019-02-05 11:02:31 +01:00
|
|
|
elif cast.media_controller.is_playing:
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.media_controller.pause()
|
2018-11-13 23:09:19 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.wait()
|
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-13 23:09:19 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def stop(self, *_, chromecast: Optional[str] = None, **__):
|
2019-06-27 23:52:40 +02:00
|
|
|
chromecast = chromecast or self.chromecast
|
2023-11-16 21:42:57 +01:00
|
|
|
if not chromecast:
|
|
|
|
return
|
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
cast = self.get_chromecast(chromecast)
|
|
|
|
cast.media_controller.stop()
|
|
|
|
cast.wait()
|
2018-11-13 23:09:19 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-13 23:09:19 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def rewind(self, chromecast: Optional[str] = None):
|
2019-06-27 23:52:40 +02:00
|
|
|
chromecast = chromecast or self.chromecast
|
|
|
|
cast = self.get_chromecast(chromecast)
|
|
|
|
cast.media_controller.rewind()
|
|
|
|
cast.wait()
|
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-13 23:09:19 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def set_position(self, position: float, chromecast: Optional[str] = None, **_):
|
2019-06-27 23:52:40 +02:00
|
|
|
cast = self.get_chromecast(chromecast or self.chromecast)
|
|
|
|
cast.media_controller.seek(position)
|
|
|
|
cast.wait()
|
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-13 23:09:19 +01:00
|
|
|
|
2019-02-05 02:30:20 +01:00
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def seek(self, position: float, chromecast: Optional[str] = None, **_):
|
2019-02-05 02:30:20 +01:00
|
|
|
return self.forward(chromecast=chromecast, offset=position)
|
2018-11-13 23:09:19 +01:00
|
|
|
|
2018-11-13 23:23:14 +01:00
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def back(self, chromecast: Optional[str] = None, offset: int = 30, **_):
|
2019-06-27 23:52:40 +02:00
|
|
|
cast = self.get_chromecast(chromecast or self.chromecast)
|
|
|
|
mc = cast.media_controller
|
2018-11-13 23:23:14 +01:00
|
|
|
if mc.status.current_time:
|
2023-09-24 16:54:43 +02:00
|
|
|
mc.seek(mc.status.current_time - offset)
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.wait()
|
2018-11-13 23:23:14 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-13 23:23:14 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def forward(self, chromecast: Optional[str] = None, offset: int = 30, **_):
|
2019-06-27 23:52:40 +02:00
|
|
|
cast = self.get_chromecast(chromecast or self.chromecast)
|
|
|
|
mc = cast.media_controller
|
2018-11-13 23:23:14 +01:00
|
|
|
if mc.status.current_time:
|
2023-09-24 16:54:43 +02:00
|
|
|
mc.seek(mc.status.current_time + offset)
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.wait()
|
2018-11-13 23:23:14 +01:00
|
|
|
|
2019-06-27 23:52:40 +02:00
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-13 23:23:14 +01:00
|
|
|
|
2018-11-13 23:09:19 +01:00
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def is_playing(self, chromecast: Optional[str] = None, **_):
|
2023-09-24 16:54:43 +02:00
|
|
|
return self.get_chromecast(
|
|
|
|
chromecast or self.chromecast
|
|
|
|
).media_controller.is_playing
|
2018-11-13 23:09:19 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def is_paused(self, chromecast: Optional[str] = None, **_):
|
2023-09-24 16:54:43 +02:00
|
|
|
return self.get_chromecast(
|
|
|
|
chromecast or self.chromecast
|
|
|
|
).media_controller.is_paused
|
2018-11-13 23:09:19 +01:00
|
|
|
|
2019-02-05 09:49:50 +01:00
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def is_idle(self, chromecast: Optional[str] = None):
|
2023-09-24 16:54:43 +02:00
|
|
|
return self.get_chromecast(
|
|
|
|
chromecast or self.chromecast
|
|
|
|
).media_controller.is_idle
|
2019-02-05 09:49:50 +01:00
|
|
|
|
2018-11-13 23:09:19 +01:00
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def list_subtitles(self, chromecast: Optional[str] = None):
|
2023-09-24 16:54:43 +02:00
|
|
|
return self.get_chromecast(
|
|
|
|
chromecast or self.chromecast
|
|
|
|
).media_controller.subtitle_tracks
|
2019-02-11 00:55:20 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def enable_subtitles(
|
|
|
|
self, chromecast: Optional[str] = None, track_id: Optional[str] = None, **_
|
|
|
|
):
|
2019-02-11 00:55:20 +01:00
|
|
|
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
|
|
|
if track_id is not None:
|
|
|
|
return mc.enable_subtitle(track_id)
|
2023-11-12 03:08:54 +01:00
|
|
|
if mc.subtitle_tracks:
|
2019-02-11 00:55:20 +01:00
|
|
|
return mc.enable_subtitle(mc.subtitle_tracks[0].get('trackId'))
|
2018-11-13 23:09:19 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def disable_subtitles(
|
|
|
|
self, chromecast: Optional[str] = None, track_id: Optional[str] = None, **_
|
|
|
|
):
|
2019-02-11 00:55:20 +01:00
|
|
|
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
2019-06-18 18:14:24 +02:00
|
|
|
if track_id:
|
|
|
|
return mc.disable_subtitle(track_id)
|
2023-11-12 03:08:54 +01:00
|
|
|
if mc.current_subtitle_tracks:
|
2019-02-11 00:55:20 +01:00
|
|
|
return mc.disable_subtitle(mc.current_subtitle_tracks[0])
|
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def toggle_subtitles(self, chromecast: Optional[str] = None, **_):
|
2019-02-11 00:55:20 +01:00
|
|
|
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
|
|
|
all_subs = mc.status.subtitle_tracks
|
|
|
|
cur_subs = mc.status.status.current_subtitle_tracks
|
|
|
|
|
|
|
|
if cur_subs:
|
2019-06-18 18:14:24 +02:00
|
|
|
return self.disable_subtitles(chromecast, cur_subs[0])
|
2018-11-13 23:09:19 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
return self.enable_subtitles(chromecast, all_subs[0].get('trackId'))
|
2018-11-13 23:09:19 +01:00
|
|
|
|
2018-11-12 16:50:20 +01:00
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def status(self, chromecast: Optional[str] = None):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2023-11-12 03:08:54 +01:00
|
|
|
:return: The status of a Chromecast (if ``chromecast`` is specified) or
|
|
|
|
all the discovered/available Chromecasts. Format:
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
.. code-block:: python
|
2023-11-18 14:17:17 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
{
|
|
|
|
"type": "cast", # Can be "cast" or "audio"
|
|
|
|
"name": "Living Room TV",
|
|
|
|
"manufacturer": "Google Inc.",
|
|
|
|
"model_name": "Chromecast",
|
|
|
|
"uuid": "f812afac-80ff-11ee-84dc-001500e8f607",
|
|
|
|
"address": "192.168.1.2",
|
|
|
|
"port": 8009,
|
|
|
|
"status": {
|
|
|
|
"app": {
|
|
|
|
"id": "CC1AD845",
|
|
|
|
"name": "Default Media Receiver"
|
|
|
|
},
|
|
|
|
"is_active_input": false,
|
|
|
|
"is_stand_by": true,
|
|
|
|
"is_idle": true,
|
|
|
|
"namespaces": [
|
|
|
|
"urn:x-cast:com.google.cast.cac",
|
|
|
|
"urn:x-cast:com.google.cast.debugoverlay",
|
|
|
|
"urn:x-cast:com.google.cast.media"
|
|
|
|
],
|
|
|
|
"volume": 100,
|
|
|
|
"muted": false,
|
|
|
|
"adjusted_current_time": 14.22972,
|
|
|
|
"album_artist": null,
|
|
|
|
"album_name": null,
|
|
|
|
"artist": null,
|
|
|
|
"url": "https://some/video.mp4",
|
|
|
|
"content_type": "video/mp4",
|
|
|
|
"current_subtitle_tracks": [],
|
|
|
|
"position": 1.411891,
|
|
|
|
"duration": 253.376145,
|
|
|
|
"episode": null,
|
|
|
|
"idle_reason": null,
|
|
|
|
"images": [
|
|
|
|
[
|
|
|
|
"https://some/image.jpg",
|
|
|
|
null,
|
|
|
|
null
|
|
|
|
]
|
|
|
|
],
|
|
|
|
"last_updated": "2023-11-12T02:03:33.888843",
|
|
|
|
"media_custom_data": {},
|
|
|
|
"media_is_generic": true,
|
|
|
|
"media_is_movie": false,
|
|
|
|
"media_is_musictrack": false,
|
|
|
|
"media_is_photo": false,
|
|
|
|
"media_is_tvshow": false,
|
|
|
|
"media_metadata": {
|
|
|
|
"title": "Some media",
|
|
|
|
"thumb": "https://some/image.jpg",
|
|
|
|
"images": [
|
|
|
|
{
|
|
|
|
"url": "https://some/image.jpg"
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"metadataType": 0
|
|
|
|
},
|
|
|
|
"media_session_id": 1,
|
|
|
|
"metadata_type": 0,
|
|
|
|
"playback_rate": 1,
|
|
|
|
"player_is_idle": false,
|
|
|
|
"player_is_paused": false,
|
|
|
|
"player_is_playing": true,
|
|
|
|
"state": "play",
|
|
|
|
"season": null,
|
|
|
|
"series_title": null,
|
|
|
|
"stream_type": "BUFFERED",
|
|
|
|
"stream_type_is_buffered": true,
|
|
|
|
"stream_type_is_live": false,
|
|
|
|
"subtitle_tracks": [],
|
|
|
|
"supported_media_commands": 12303,
|
|
|
|
"supports_pause": true,
|
|
|
|
"supports_queue_next": false,
|
|
|
|
"supports_queue_prev": false,
|
|
|
|
"seekable": true,
|
|
|
|
"supports_skip_backward": false,
|
|
|
|
"supports_skip_forward": false,
|
|
|
|
"supports_stream_mute": true,
|
|
|
|
"supports_stream_volume": true,
|
|
|
|
"title": "Some media",
|
|
|
|
"track": null
|
|
|
|
}
|
|
|
|
}
|
2023-11-18 14:17:17 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
"""
|
|
|
|
return self._status(chromecast=chromecast)
|
|
|
|
|
|
|
|
def _status(self, chromecast: Optional[str] = None) -> dict:
|
|
|
|
if chromecast:
|
|
|
|
assert (
|
|
|
|
chromecast in self.chromecasts
|
|
|
|
), f'No such Chromecast device: {chromecast}'
|
|
|
|
return self._serialize_device(self.chromecasts[chromecast])
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
return {
|
|
|
|
name: self._serialize_device(cast)
|
|
|
|
for name, cast in self.chromecasts.items()
|
|
|
|
}
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
@action
|
|
|
|
def disconnect(
|
|
|
|
self,
|
|
|
|
chromecast: Optional[str] = None,
|
|
|
|
timeout: Optional[float] = None,
|
|
|
|
blocking: bool = True,
|
|
|
|
):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2023-11-12 03:08:54 +01:00
|
|
|
Disconnect a Chromecast and wait for it to terminate
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
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.
|
|
|
|
"""
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2018-11-12 16:50:20 +01:00
|
|
|
cast.disconnect(timeout=timeout, blocking=blocking)
|
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def join(self, chromecast: Optional[str] = None, timeout: Optional[float] = None):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
|
|
|
Blocks the thread until the Chromecast connection is terminated.
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
the default configured Chromecast will be used.
|
|
|
|
:param timeout: Number of seconds to wait for disconnection (default:
|
|
|
|
None: block until termination).
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.join(timeout=timeout)
|
2018-11-12 16:50:20 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def quit(self, chromecast: Optional[str] = None):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
|
|
|
Exits the current app on the Chromecast
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
the default configured Chromecast will be used.
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2018-11-12 16:50:20 +01:00
|
|
|
cast.quit_app()
|
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def reboot(self, chromecast: Optional[str] = None):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
|
|
|
Reboots the Chromecast
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
the default configured Chromecast will be used.
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2018-11-12 16:50:20 +01:00
|
|
|
cast.reboot()
|
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def set_volume(self, volume: float, chromecast: Optional[str] = None):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
|
|
|
Set the Chromecast volume
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
:param volume: Volume to be set, between 0 and 100.
|
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
the default configured Chromecast will be used.
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2019-06-27 23:52:40 +02:00
|
|
|
chromecast = chromecast or self.chromecast
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2023-09-24 16:54:43 +02:00
|
|
|
cast.set_volume(volume / 100)
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.wait()
|
2023-11-12 03:08:54 +01:00
|
|
|
return {
|
|
|
|
**self._status(chromecast=chromecast),
|
|
|
|
'volume': volume,
|
|
|
|
}
|
2018-11-12 16:50:20 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def volup(self, chromecast: Optional[str] = None, step: float = 10, **_):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2019-02-05 02:30:20 +01:00
|
|
|
Turn up the Chromecast volume by 10% or step.
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
the default configured Chromecast will be used.
|
|
|
|
:param step: Volume increment between 0 and 100 (default: 10%).
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2019-06-27 23:52:40 +02:00
|
|
|
chromecast = chromecast or self.chromecast
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2019-02-05 02:30:20 +01:00
|
|
|
step /= 100
|
|
|
|
cast.volume_up(min(step, 1))
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.wait()
|
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-12 16:50:20 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def voldown(self, chromecast: Optional[str] = None, step: float = 10, **_):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2019-02-05 02:30:20 +01:00
|
|
|
Turn down the Chromecast volume by 10% or step.
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
the default configured Chromecast will be used.
|
|
|
|
:param step: Volume decrement between 0 and 100 (default: 10%).
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2019-06-27 23:52:40 +02:00
|
|
|
chromecast = chromecast or self.chromecast
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2019-02-05 02:30:20 +01:00
|
|
|
step /= 100
|
|
|
|
cast.volume_down(max(step, 0))
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.wait()
|
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-12 16:50:20 +01:00
|
|
|
|
|
|
|
@action
|
2023-11-12 03:08:54 +01:00
|
|
|
def mute(self, chromecast: Optional[str] = None):
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
|
|
|
Toggle the mute status on the Chromecast
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
:param chromecast: Chromecast to cast to. If none is specified, then
|
|
|
|
the default configured Chromecast will be used.
|
2018-11-12 16:50:20 +01:00
|
|
|
"""
|
2019-06-27 23:52:40 +02:00
|
|
|
chromecast = chromecast or self.chromecast
|
2018-11-13 01:29:24 +01:00
|
|
|
cast = self.get_chromecast(chromecast)
|
2018-11-12 16:50:20 +01:00
|
|
|
cast.set_volume_muted(not cast.status.volume_muted)
|
2019-06-27 23:52:40 +02:00
|
|
|
cast.wait()
|
|
|
|
return self.status(chromecast=chromecast)
|
2018-11-12 16:50:20 +01:00
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
def set_subtitles(self, *_, **__):
|
2021-09-16 17:53:40 +02:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
def remove_subtitles(self, *_, **__):
|
2021-09-16 17:53:40 +02:00
|
|
|
raise NotImplementedError
|
|
|
|
|
2023-11-12 03:08:54 +01:00
|
|
|
def main(self):
|
|
|
|
while not self.should_stop():
|
|
|
|
try:
|
|
|
|
self._refresh_chromecasts()
|
|
|
|
finally:
|
|
|
|
self.wait_stop(self.poll_interval)
|
|
|
|
|
2018-11-12 16:50:20 +01:00
|
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|