mirror of
https://github.com/CCOSTAN/Home-AssistantConfig.git
synced 2025-11-06 09:45:07 +00:00
Initial Configuration Push
This commit is contained in:
BIN
deps/pychromecast/.DS_Store
vendored
Normal file
BIN
deps/pychromecast/.DS_Store
vendored
Normal file
Binary file not shown.
424
deps/pychromecast/__init__.py
vendored
Normal file
424
deps/pychromecast/__init__.py
vendored
Normal file
@@ -0,0 +1,424 @@
|
||||
"""
|
||||
PyChromecast: remote control your Chromecast
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import fnmatch
|
||||
|
||||
# pylint: disable=wildcard-import
|
||||
import threading
|
||||
from .config import * # noqa
|
||||
from .error import * # noqa
|
||||
from . import socket_client
|
||||
from .discovery import discover_chromecasts
|
||||
from .dial import get_device_status, reboot, DeviceStatus, CAST_TYPES, \
|
||||
CAST_TYPE_CHROMECAST
|
||||
from .controllers.media import STREAM_TYPE_BUFFERED # noqa
|
||||
|
||||
__all__ = (
|
||||
'__version__', '__version_info__',
|
||||
'get_chromecasts', 'get_chromecasts_as_dict', 'get_chromecast',
|
||||
'Chromecast'
|
||||
)
|
||||
__version_info__ = ('0', '7', '1')
|
||||
__version__ = '.'.join(__version_info__)
|
||||
|
||||
IDLE_APP_ID = 'E8C28D3C'
|
||||
IGNORE_CEC = []
|
||||
# For Python 2.x we need to decode __repr__ Unicode return values to str
|
||||
NON_UNICODE_REPR = sys.version_info < (3, )
|
||||
|
||||
|
||||
def _get_all_chromecasts(tries=None, retry_wait=None, timeout=None):
|
||||
"""
|
||||
Returns a list of all chromecasts on the network as PyChromecast
|
||||
objects.
|
||||
"""
|
||||
hosts = discover_chromecasts()
|
||||
cc_list = []
|
||||
for ip_address, port, uuid, model_name, friendly_name in hosts:
|
||||
try:
|
||||
# Build device status from the mDNS info, this information is
|
||||
# the primary source and the remaining will be fetched
|
||||
# later on.
|
||||
cast_type = CAST_TYPES.get(model_name.lower(),
|
||||
CAST_TYPE_CHROMECAST)
|
||||
device = DeviceStatus(
|
||||
friendly_name=friendly_name, model_name=model_name,
|
||||
manufacturer=None, api_version=None,
|
||||
uuid=uuid, cast_type=cast_type,
|
||||
)
|
||||
cc_list.append(Chromecast(host=ip_address, port=port,
|
||||
device=device,
|
||||
tries=tries, timeout=timeout,
|
||||
retry_wait=retry_wait))
|
||||
except ChromecastConnectionError: # noqa
|
||||
pass
|
||||
return cc_list
|
||||
|
||||
|
||||
def get_chromecasts(tries=None, retry_wait=None, timeout=None, **filters):
|
||||
"""
|
||||
Searches the network and returns a list of Chromecast objects.
|
||||
Filter is a list of options to filter the chromecasts by.
|
||||
|
||||
ex: get_chromecasts(friendly_name="Living Room")
|
||||
|
||||
May return an empty list if no chromecasts were found matching
|
||||
the filter criteria
|
||||
|
||||
Filters include DeviceStatus items:
|
||||
friendly_name, model_name, manufacturer, api_version
|
||||
Or AppStatus items:
|
||||
app_id, description, state, service_url, service_protocols (list)
|
||||
Or ip address:
|
||||
ip
|
||||
|
||||
Tries is specified if you want to limit the number of times the
|
||||
underlying socket associated with your Chromecast objects will
|
||||
retry connecting if connection is lost or it fails to connect
|
||||
in the first place. The number of seconds spent between each retry
|
||||
can be defined by passing the retry_wait parameter, the default is
|
||||
to wait 5 seconds.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
cc_list = set(_get_all_chromecasts(tries, retry_wait, timeout))
|
||||
excluded_cc = set()
|
||||
|
||||
if not filters:
|
||||
return list(cc_list)
|
||||
|
||||
if 'ip' in filters:
|
||||
for chromecast in cc_list:
|
||||
if chromecast.host != filters['ip']:
|
||||
excluded_cc.add(chromecast)
|
||||
filters.pop('ip')
|
||||
|
||||
for key, val in filters.items():
|
||||
for chromecast in cc_list:
|
||||
for tup in [chromecast.device, chromecast.status]:
|
||||
if hasattr(tup, key) and val != getattr(tup, key):
|
||||
excluded_cc.add(chromecast)
|
||||
|
||||
filtered_cc = cc_list - excluded_cc
|
||||
|
||||
for cast in excluded_cc:
|
||||
logger.debug("Stopping excluded chromecast %s", cast)
|
||||
cast.socket_client.stop.set()
|
||||
|
||||
return list(filtered_cc)
|
||||
|
||||
|
||||
def get_chromecasts_as_dict(tries=None, retry_wait=None, timeout=None,
|
||||
**filters):
|
||||
"""
|
||||
Returns a dictionary of chromecasts with the friendly name as
|
||||
the key. The value is the pychromecast object itself.
|
||||
|
||||
Tries is specified if you want to limit the number of times the
|
||||
underlying socket associated with your Chromecast objects will
|
||||
retry connecting if connection is lost or it fails to connect
|
||||
in the first place. The number of seconds spent between each retry
|
||||
can be defined by passing the retry_wait parameter, the default is
|
||||
to wait 5 seconds.
|
||||
"""
|
||||
return {cc.device.friendly_name: cc
|
||||
for cc in get_chromecasts(tries=tries, retry_wait=retry_wait,
|
||||
timeout=timeout,
|
||||
**filters)}
|
||||
|
||||
|
||||
def get_chromecast(strict=False, tries=None, retry_wait=None, timeout=None,
|
||||
**filters):
|
||||
"""
|
||||
Same as get_chromecasts but only if filter matches exactly one
|
||||
ChromeCast.
|
||||
|
||||
Returns a Chromecast matching exactly the fitler specified.
|
||||
|
||||
If strict, return one and only one chromecast
|
||||
|
||||
Tries is specified if you want to limit the number of times the
|
||||
underlying socket associated with your Chromecast objects will
|
||||
retry connecting if connection is lost or it fails to connect
|
||||
in the first place. The number of seconds spent between each retry
|
||||
can be defined by passing the retry_wait parameter, the default is
|
||||
to wait 5 seconds.
|
||||
|
||||
:type retry_wait: float or None
|
||||
"""
|
||||
|
||||
# If we have filters or are operating in strict mode we have to scan
|
||||
# for all Chromecasts to ensure there is only 1 matching chromecast.
|
||||
# If no filters given and not strict just use the first dicsovered one.
|
||||
if filters or strict:
|
||||
results = get_chromecasts(tries=tries, retry_wait=retry_wait,
|
||||
timeout=timeout,
|
||||
**filters)
|
||||
else:
|
||||
results = _get_all_chromecasts(tries, retry_wait)
|
||||
|
||||
if len(results) > 1:
|
||||
if strict:
|
||||
raise MultipleChromecastsFoundError( # noqa
|
||||
'More than one Chromecast was found specifying '
|
||||
'the filter criteria: {}'.format(filters))
|
||||
else:
|
||||
return results[0]
|
||||
|
||||
elif not results:
|
||||
if strict:
|
||||
raise NoChromecastFoundError( # noqa
|
||||
'No Chromecasts matching filter critera were found:'
|
||||
' {}'.format(filters))
|
||||
else:
|
||||
return None
|
||||
|
||||
else:
|
||||
return results[0]
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class Chromecast(object):
|
||||
"""
|
||||
Class to interface with a ChromeCast.
|
||||
|
||||
:param port: The port to use when connecting to the device, set to None to
|
||||
use the default of 8009. Special devices such as Cast Groups
|
||||
may return a different port number so we need to use that.
|
||||
:param device: DeviceStatus with initial information for the device.
|
||||
:type device: pychromecast.dial.DeviceStatus
|
||||
:param tries: Number of retries to perform if the connection fails.
|
||||
None for inifinite retries.
|
||||
:param timeout: A floating point number specifying the socket timeout in
|
||||
seconds. None means to use the default which is 30 seconds.
|
||||
:param retry_wait: A floating point number specifying how many seconds to
|
||||
wait between each retry. None means to use the default
|
||||
which is 5 seconds.
|
||||
"""
|
||||
|
||||
def __init__(self, host, port=None, device=None, **kwargs):
|
||||
tries = kwargs.pop('tries', None)
|
||||
timeout = kwargs.pop('timeout', None)
|
||||
retry_wait = kwargs.pop('retry_wait', None)
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Resolve host to IP address
|
||||
self.host = host
|
||||
self.port = port or 8009
|
||||
|
||||
self.logger.info("Querying device status")
|
||||
self.device = device
|
||||
if device:
|
||||
dev_status = get_device_status(self.host)
|
||||
if dev_status:
|
||||
# Values from `device` have priority over `dev_status`
|
||||
# as they come from the dial information.
|
||||
# `dev_status` may add extra information such as `manufacturer`
|
||||
# which dial does not supply
|
||||
self.device = DeviceStatus(
|
||||
friendly_name=(device.friendly_name or
|
||||
dev_status.friendly_name),
|
||||
model_name=(device.model_name or
|
||||
dev_status.model_name),
|
||||
manufacturer=(device.manufacturer or
|
||||
dev_status.manufacturer),
|
||||
api_version=(device.api_version or
|
||||
dev_status.api_version),
|
||||
uuid=(device.uuid or
|
||||
dev_status.uuid),
|
||||
cast_type=(device.cast_type or
|
||||
dev_status.cast_type),
|
||||
)
|
||||
else:
|
||||
self.device = device
|
||||
else:
|
||||
self.device = get_device_status(self.host)
|
||||
|
||||
if not self.device:
|
||||
raise ChromecastConnectionError( # noqa
|
||||
"Could not connect to {}:{}".format(self.host, self.port))
|
||||
|
||||
self.status = None
|
||||
self.status_event = threading.Event()
|
||||
|
||||
self.socket_client = socket_client.SocketClient(
|
||||
host, port=port, cast_type=self.device.cast_type,
|
||||
tries=tries, timeout=timeout, retry_wait=retry_wait)
|
||||
|
||||
receiver_controller = self.socket_client.receiver_controller
|
||||
receiver_controller.register_status_listener(self)
|
||||
|
||||
# Forward these methods
|
||||
self.set_volume = receiver_controller.set_volume
|
||||
self.set_volume_muted = receiver_controller.set_volume_muted
|
||||
self.play_media = self.socket_client.media_controller.play_media
|
||||
self.register_handler = self.socket_client.register_handler
|
||||
self.register_status_listener = \
|
||||
receiver_controller.register_status_listener
|
||||
self.register_launch_error_listener = \
|
||||
receiver_controller.register_launch_error_listener
|
||||
self.register_connection_listener = \
|
||||
self.socket_client.register_connection_listener
|
||||
|
||||
self.socket_client.start()
|
||||
|
||||
@property
|
||||
def ignore_cec(self):
|
||||
""" Returns whether the CEC data should be ignored. """
|
||||
return self.device is not None and \
|
||||
any([fnmatch.fnmatchcase(self.device.friendly_name, pattern)
|
||||
for pattern in IGNORE_CEC])
|
||||
|
||||
@property
|
||||
def is_idle(self):
|
||||
""" Returns if there is currently an app running. """
|
||||
return (self.status is None or
|
||||
self.app_id in (None, IDLE_APP_ID) or
|
||||
(not self.status.is_active_input and not self.ignore_cec))
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
""" Returns the unique UUID of the Chromecast device. """
|
||||
return self.device.uuid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Returns the friendly name set for the Chromecast device.
|
||||
This is the name that the end-user chooses for the cast device.
|
||||
"""
|
||||
return self.device.friendly_name
|
||||
|
||||
@property
|
||||
def model_name(self):
|
||||
""" Returns the model name of the Chromecast device. """
|
||||
return self.device.model_name
|
||||
|
||||
@property
|
||||
def cast_type(self):
|
||||
"""
|
||||
Returns the type of the Chromecast device.
|
||||
This is one of CAST_TYPE_CHROMECAST for regular Chromecast device,
|
||||
CAST_TYPE_AUDIO for Chromecast devices that only support audio
|
||||
and CAST_TYPE_GROUP for virtual a Chromecast device that groups
|
||||
together two or more cast (Audio for now) devices.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
return self.device.cast_type
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
""" Returns the current app_id. """
|
||||
return self.status.app_id if self.status else None
|
||||
|
||||
@property
|
||||
def app_display_name(self):
|
||||
""" Returns the name of the current running app. """
|
||||
return self.status.display_name if self.status else None
|
||||
|
||||
@property
|
||||
def media_controller(self):
|
||||
""" Returns the media controller. """
|
||||
return self.socket_client.media_controller
|
||||
|
||||
def new_cast_status(self, status):
|
||||
""" Called when a new status received from the Chromecast. """
|
||||
self.status = status
|
||||
if status:
|
||||
self.status_event.set()
|
||||
|
||||
def start_app(self, app_id):
|
||||
""" Start an app on the Chromecast. """
|
||||
self.logger.info("Starting app %s", app_id)
|
||||
|
||||
self.socket_client.receiver_controller.launch_app(app_id)
|
||||
|
||||
def quit_app(self):
|
||||
""" Tells the Chromecast to quit current app_id. """
|
||||
self.logger.info("Quiting current app")
|
||||
|
||||
self.socket_client.receiver_controller.stop_app()
|
||||
|
||||
def reboot(self):
|
||||
""" Reboots the Chromecast. """
|
||||
reboot(self.host)
|
||||
|
||||
def volume_up(self):
|
||||
""" Increment volume by 0.1 unless it is already maxed.
|
||||
Returns the new volume.
|
||||
|
||||
"""
|
||||
volume = round(self.status.volume_level, 1)
|
||||
return self.set_volume(volume + 0.1)
|
||||
|
||||
def volume_down(self):
|
||||
""" Decrement the volume by 0.1 unless it is already 0.
|
||||
Returns the new volume.
|
||||
"""
|
||||
volume = round(self.status.volume_level, 1)
|
||||
return self.set_volume(volume - 0.1)
|
||||
|
||||
def wait(self, timeout=None):
|
||||
"""
|
||||
Waits until the cast device is ready for communication. The device
|
||||
is ready as soon a status message has been received.
|
||||
|
||||
If the status has already been received then the method returns
|
||||
immediately.
|
||||
|
||||
:param timeout: a floating point number specifying a timeout for the
|
||||
operation in seconds (or fractions thereof). Or None
|
||||
to block forever.
|
||||
"""
|
||||
self.status_event.wait(timeout=timeout)
|
||||
|
||||
def disconnect(self, timeout=None, blocking=True):
|
||||
"""
|
||||
Disconnects the chromecast and waits for it to terminate.
|
||||
|
||||
:param timeout: a floating point number specifying a timeout for the
|
||||
operation in seconds (or fractions thereof). Or None
|
||||
to block forever.
|
||||
:param blocking: If True it will block until the disconnection is
|
||||
complete, otherwise it will return immediately.
|
||||
"""
|
||||
self.socket_client.disconnect()
|
||||
if blocking:
|
||||
self.join(timeout=timeout)
|
||||
|
||||
def join(self, timeout=None):
|
||||
"""
|
||||
Blocks the thread of the caller until the chromecast connection is
|
||||
stopped.
|
||||
|
||||
:param timeout: a floating point number specifying a timeout for the
|
||||
operation in seconds (or fractions thereof). Or None
|
||||
to block forever.
|
||||
"""
|
||||
self.socket_client.join(timeout=timeout)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.socket_client.stop.set()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
txt = u"Chromecast({!r}, port={!r}, device={!r})".format(
|
||||
self.host, self.port, self.device)
|
||||
# Python 2.x does not work well with unicode returned from repr
|
||||
if NON_UNICODE_REPR:
|
||||
return txt.encode('utf-8')
|
||||
return txt
|
||||
|
||||
def __unicode__(self):
|
||||
return u"Chromecast({}, {}, {}, {}, {}, api={}.{})".format(
|
||||
self.host, self.port, self.device.friendly_name,
|
||||
self.device.model_name, self.device.manufacturer,
|
||||
self.device.api_version[0], self.device.api_version[1])
|
||||
BIN
deps/pychromecast/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/authority_keys_pb2.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/authority_keys_pb2.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/cast_channel_pb2.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/cast_channel_pb2.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/config.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/config.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/dial.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/dial.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/discovery.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/discovery.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/error.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/error.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/logging_pb2.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/logging_pb2.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/socket_client.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/socket_client.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/upnp.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/upnp.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/websocket.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/websocket.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/__pycache__/youtube.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/__pycache__/youtube.cpython-34.pyc
vendored
Normal file
Binary file not shown.
118
deps/pychromecast/authority_keys_pb2.py
vendored
Normal file
118
deps/pychromecast/authority_keys_pb2.py
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: authority_keys.proto
|
||||
|
||||
import sys
|
||||
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf import descriptor_pb2
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='authority_keys.proto',
|
||||
package='extensions.core_api.cast_channel.proto',
|
||||
syntax='proto2',
|
||||
serialized_pb=_b('\n\x14\x61uthority_keys.proto\x12&extensions.core_api.cast_channel.proto\"\x88\x01\n\rAuthorityKeys\x12G\n\x04keys\x18\x01 \x03(\x0b\x32\x39.extensions.core_api.cast_channel.proto.AuthorityKeys.Key\x1a.\n\x03Key\x12\x13\n\x0b\x66ingerprint\x18\x01 \x02(\x0c\x12\x12\n\npublic_key\x18\x02 \x02(\x0c\x42\x02H\x03')
|
||||
)
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
|
||||
|
||||
|
||||
_AUTHORITYKEYS_KEY = _descriptor.Descriptor(
|
||||
name='Key',
|
||||
full_name='extensions.core_api.cast_channel.proto.AuthorityKeys.Key',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fingerprint', full_name='extensions.core_api.cast_channel.proto.AuthorityKeys.Key.fingerprint', index=0,
|
||||
number=1, type=12, cpp_type=9, label=2,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='public_key', full_name='extensions.core_api.cast_channel.proto.AuthorityKeys.Key.public_key', index=1,
|
||||
number=2, type=12, cpp_type=9, label=2,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto2',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=155,
|
||||
serialized_end=201,
|
||||
)
|
||||
|
||||
_AUTHORITYKEYS = _descriptor.Descriptor(
|
||||
name='AuthorityKeys',
|
||||
full_name='extensions.core_api.cast_channel.proto.AuthorityKeys',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='keys', full_name='extensions.core_api.cast_channel.proto.AuthorityKeys.keys', index=0,
|
||||
number=1, type=11, cpp_type=10, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[_AUTHORITYKEYS_KEY, ],
|
||||
enum_types=[
|
||||
],
|
||||
options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto2',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=65,
|
||||
serialized_end=201,
|
||||
)
|
||||
|
||||
_AUTHORITYKEYS_KEY.containing_type = _AUTHORITYKEYS
|
||||
_AUTHORITYKEYS.fields_by_name['keys'].message_type = _AUTHORITYKEYS_KEY
|
||||
DESCRIPTOR.message_types_by_name['AuthorityKeys'] = _AUTHORITYKEYS
|
||||
|
||||
AuthorityKeys = _reflection.GeneratedProtocolMessageType('AuthorityKeys', (_message.Message,), dict(
|
||||
|
||||
Key = _reflection.GeneratedProtocolMessageType('Key', (_message.Message,), dict(
|
||||
DESCRIPTOR = _AUTHORITYKEYS_KEY,
|
||||
__module__ = 'authority_keys_pb2'
|
||||
# @@protoc_insertion_point(class_scope:extensions.core_api.cast_channel.proto.AuthorityKeys.Key)
|
||||
))
|
||||
,
|
||||
DESCRIPTOR = _AUTHORITYKEYS,
|
||||
__module__ = 'authority_keys_pb2'
|
||||
# @@protoc_insertion_point(class_scope:extensions.core_api.cast_channel.proto.AuthorityKeys)
|
||||
))
|
||||
_sym_db.RegisterMessage(AuthorityKeys)
|
||||
_sym_db.RegisterMessage(AuthorityKeys.Key)
|
||||
|
||||
|
||||
DESCRIPTOR.has_options = True
|
||||
DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003'))
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
416
deps/pychromecast/cast_channel_pb2.py
vendored
Normal file
416
deps/pychromecast/cast_channel_pb2.py
vendored
Normal file
@@ -0,0 +1,416 @@
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: cast_channel.proto
|
||||
|
||||
import sys
|
||||
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
|
||||
from google.protobuf.internal import enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf import descriptor_pb2
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='cast_channel.proto',
|
||||
package='extensions.core_api.cast_channel',
|
||||
syntax='proto2',
|
||||
serialized_pb=_b('\n\x12\x63\x61st_channel.proto\x12 extensions.core_api.cast_channel\"\xed\x02\n\x0b\x43\x61stMessage\x12W\n\x10protocol_version\x18\x01 \x02(\x0e\x32=.extensions.core_api.cast_channel.CastMessage.ProtocolVersion\x12\x11\n\tsource_id\x18\x02 \x02(\t\x12\x16\n\x0e\x64\x65stination_id\x18\x03 \x02(\t\x12\x11\n\tnamespace\x18\x04 \x02(\t\x12O\n\x0cpayload_type\x18\x05 \x02(\x0e\x32\x39.extensions.core_api.cast_channel.CastMessage.PayloadType\x12\x14\n\x0cpayload_utf8\x18\x06 \x01(\t\x12\x16\n\x0epayload_binary\x18\x07 \x01(\x0c\"!\n\x0fProtocolVersion\x12\x0e\n\nCASTV2_1_0\x10\x00\"%\n\x0bPayloadType\x12\n\n\x06STRING\x10\x00\x12\n\n\x06\x42INARY\x10\x01\"s\n\rAuthChallenge\x12\x62\n\x13signature_algorithm\x18\x01 \x01(\x0e\x32\x34.extensions.core_api.cast_channel.SignatureAlgorithm:\x0fRSASSA_PKCS1v15\"\xc8\x01\n\x0c\x41uthResponse\x12\x11\n\tsignature\x18\x01 \x02(\x0c\x12\x1f\n\x17\x63lient_auth_certificate\x18\x02 \x02(\x0c\x12 \n\x18intermediate_certificate\x18\x03 \x03(\x0c\x12\x62\n\x13signature_algorithm\x18\x04 \x01(\x0e\x32\x34.extensions.core_api.cast_channel.SignatureAlgorithm:\x0fRSASSA_PKCS1v15\"\xa8\x01\n\tAuthError\x12I\n\nerror_type\x18\x01 \x02(\x0e\x32\x35.extensions.core_api.cast_channel.AuthError.ErrorType\"P\n\tErrorType\x12\x12\n\x0eINTERNAL_ERROR\x10\x00\x12\n\n\x06NO_TLS\x10\x01\x12#\n\x1fSIGNATURE_ALGORITHM_UNAVAILABLE\x10\x02\"\xd5\x01\n\x11\x44\x65viceAuthMessage\x12\x42\n\tchallenge\x18\x01 \x01(\x0b\x32/.extensions.core_api.cast_channel.AuthChallenge\x12@\n\x08response\x18\x02 \x01(\x0b\x32..extensions.core_api.cast_channel.AuthResponse\x12:\n\x05\x65rror\x18\x03 \x01(\x0b\x32+.extensions.core_api.cast_channel.AuthError*J\n\x12SignatureAlgorithm\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x13\n\x0fRSASSA_PKCS1v15\x10\x01\x12\x0e\n\nRSASSA_PSS\x10\x02\x42\x02H\x03')
|
||||
)
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
_SIGNATUREALGORITHM = _descriptor.EnumDescriptor(
|
||||
name='SignatureAlgorithm',
|
||||
full_name='extensions.core_api.cast_channel.SignatureAlgorithm',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='UNSPECIFIED', index=0, number=0,
|
||||
options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='RSASSA_PKCS1v15', index=1, number=1,
|
||||
options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='RSASSA_PSS', index=2, number=2,
|
||||
options=None,
|
||||
type=None),
|
||||
],
|
||||
containing_type=None,
|
||||
options=None,
|
||||
serialized_start=1131,
|
||||
serialized_end=1205,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_SIGNATUREALGORITHM)
|
||||
|
||||
SignatureAlgorithm = enum_type_wrapper.EnumTypeWrapper(_SIGNATUREALGORITHM)
|
||||
UNSPECIFIED = 0
|
||||
RSASSA_PKCS1v15 = 1
|
||||
RSASSA_PSS = 2
|
||||
|
||||
|
||||
_CASTMESSAGE_PROTOCOLVERSION = _descriptor.EnumDescriptor(
|
||||
name='ProtocolVersion',
|
||||
full_name='extensions.core_api.cast_channel.CastMessage.ProtocolVersion',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CASTV2_1_0', index=0, number=0,
|
||||
options=None,
|
||||
type=None),
|
||||
],
|
||||
containing_type=None,
|
||||
options=None,
|
||||
serialized_start=350,
|
||||
serialized_end=383,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_CASTMESSAGE_PROTOCOLVERSION)
|
||||
|
||||
_CASTMESSAGE_PAYLOADTYPE = _descriptor.EnumDescriptor(
|
||||
name='PayloadType',
|
||||
full_name='extensions.core_api.cast_channel.CastMessage.PayloadType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='STRING', index=0, number=0,
|
||||
options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='BINARY', index=1, number=1,
|
||||
options=None,
|
||||
type=None),
|
||||
],
|
||||
containing_type=None,
|
||||
options=None,
|
||||
serialized_start=385,
|
||||
serialized_end=422,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_CASTMESSAGE_PAYLOADTYPE)
|
||||
|
||||
_AUTHERROR_ERRORTYPE = _descriptor.EnumDescriptor(
|
||||
name='ErrorType',
|
||||
full_name='extensions.core_api.cast_channel.AuthError.ErrorType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='INTERNAL_ERROR', index=0, number=0,
|
||||
options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='NO_TLS', index=1, number=1,
|
||||
options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SIGNATURE_ALGORITHM_UNAVAILABLE', index=2, number=2,
|
||||
options=None,
|
||||
type=None),
|
||||
],
|
||||
containing_type=None,
|
||||
options=None,
|
||||
serialized_start=833,
|
||||
serialized_end=913,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_AUTHERROR_ERRORTYPE)
|
||||
|
||||
|
||||
_CASTMESSAGE = _descriptor.Descriptor(
|
||||
name='CastMessage',
|
||||
full_name='extensions.core_api.cast_channel.CastMessage',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='protocol_version', full_name='extensions.core_api.cast_channel.CastMessage.protocol_version', index=0,
|
||||
number=1, type=14, cpp_type=8, label=2,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='source_id', full_name='extensions.core_api.cast_channel.CastMessage.source_id', index=1,
|
||||
number=2, type=9, cpp_type=9, label=2,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='destination_id', full_name='extensions.core_api.cast_channel.CastMessage.destination_id', index=2,
|
||||
number=3, type=9, cpp_type=9, label=2,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='namespace', full_name='extensions.core_api.cast_channel.CastMessage.namespace', index=3,
|
||||
number=4, type=9, cpp_type=9, label=2,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='payload_type', full_name='extensions.core_api.cast_channel.CastMessage.payload_type', index=4,
|
||||
number=5, type=14, cpp_type=8, label=2,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='payload_utf8', full_name='extensions.core_api.cast_channel.CastMessage.payload_utf8', index=5,
|
||||
number=6, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='payload_binary', full_name='extensions.core_api.cast_channel.CastMessage.payload_binary', index=6,
|
||||
number=7, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
_CASTMESSAGE_PROTOCOLVERSION,
|
||||
_CASTMESSAGE_PAYLOADTYPE,
|
||||
],
|
||||
options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto2',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=57,
|
||||
serialized_end=422,
|
||||
)
|
||||
|
||||
|
||||
_AUTHCHALLENGE = _descriptor.Descriptor(
|
||||
name='AuthChallenge',
|
||||
full_name='extensions.core_api.cast_channel.AuthChallenge',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='signature_algorithm', full_name='extensions.core_api.cast_channel.AuthChallenge.signature_algorithm', index=0,
|
||||
number=1, type=14, cpp_type=8, label=1,
|
||||
has_default_value=True, default_value=1,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto2',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=424,
|
||||
serialized_end=539,
|
||||
)
|
||||
|
||||
|
||||
_AUTHRESPONSE = _descriptor.Descriptor(
|
||||
name='AuthResponse',
|
||||
full_name='extensions.core_api.cast_channel.AuthResponse',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='signature', full_name='extensions.core_api.cast_channel.AuthResponse.signature', index=0,
|
||||
number=1, type=12, cpp_type=9, label=2,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='client_auth_certificate', full_name='extensions.core_api.cast_channel.AuthResponse.client_auth_certificate', index=1,
|
||||
number=2, type=12, cpp_type=9, label=2,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='intermediate_certificate', full_name='extensions.core_api.cast_channel.AuthResponse.intermediate_certificate', index=2,
|
||||
number=3, type=12, cpp_type=9, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='signature_algorithm', full_name='extensions.core_api.cast_channel.AuthResponse.signature_algorithm', index=3,
|
||||
number=4, type=14, cpp_type=8, label=1,
|
||||
has_default_value=True, default_value=1,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto2',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=542,
|
||||
serialized_end=742,
|
||||
)
|
||||
|
||||
|
||||
_AUTHERROR = _descriptor.Descriptor(
|
||||
name='AuthError',
|
||||
full_name='extensions.core_api.cast_channel.AuthError',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='error_type', full_name='extensions.core_api.cast_channel.AuthError.error_type', index=0,
|
||||
number=1, type=14, cpp_type=8, label=2,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
_AUTHERROR_ERRORTYPE,
|
||||
],
|
||||
options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto2',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=745,
|
||||
serialized_end=913,
|
||||
)
|
||||
|
||||
|
||||
_DEVICEAUTHMESSAGE = _descriptor.Descriptor(
|
||||
name='DeviceAuthMessage',
|
||||
full_name='extensions.core_api.cast_channel.DeviceAuthMessage',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='challenge', full_name='extensions.core_api.cast_channel.DeviceAuthMessage.challenge', index=0,
|
||||
number=1, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='response', full_name='extensions.core_api.cast_channel.DeviceAuthMessage.response', index=1,
|
||||
number=2, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='error', full_name='extensions.core_api.cast_channel.DeviceAuthMessage.error', index=2,
|
||||
number=3, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto2',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=916,
|
||||
serialized_end=1129,
|
||||
)
|
||||
|
||||
_CASTMESSAGE.fields_by_name['protocol_version'].enum_type = _CASTMESSAGE_PROTOCOLVERSION
|
||||
_CASTMESSAGE.fields_by_name['payload_type'].enum_type = _CASTMESSAGE_PAYLOADTYPE
|
||||
_CASTMESSAGE_PROTOCOLVERSION.containing_type = _CASTMESSAGE
|
||||
_CASTMESSAGE_PAYLOADTYPE.containing_type = _CASTMESSAGE
|
||||
_AUTHCHALLENGE.fields_by_name['signature_algorithm'].enum_type = _SIGNATUREALGORITHM
|
||||
_AUTHRESPONSE.fields_by_name['signature_algorithm'].enum_type = _SIGNATUREALGORITHM
|
||||
_AUTHERROR.fields_by_name['error_type'].enum_type = _AUTHERROR_ERRORTYPE
|
||||
_AUTHERROR_ERRORTYPE.containing_type = _AUTHERROR
|
||||
_DEVICEAUTHMESSAGE.fields_by_name['challenge'].message_type = _AUTHCHALLENGE
|
||||
_DEVICEAUTHMESSAGE.fields_by_name['response'].message_type = _AUTHRESPONSE
|
||||
_DEVICEAUTHMESSAGE.fields_by_name['error'].message_type = _AUTHERROR
|
||||
DESCRIPTOR.message_types_by_name['CastMessage'] = _CASTMESSAGE
|
||||
DESCRIPTOR.message_types_by_name['AuthChallenge'] = _AUTHCHALLENGE
|
||||
DESCRIPTOR.message_types_by_name['AuthResponse'] = _AUTHRESPONSE
|
||||
DESCRIPTOR.message_types_by_name['AuthError'] = _AUTHERROR
|
||||
DESCRIPTOR.message_types_by_name['DeviceAuthMessage'] = _DEVICEAUTHMESSAGE
|
||||
DESCRIPTOR.enum_types_by_name['SignatureAlgorithm'] = _SIGNATUREALGORITHM
|
||||
|
||||
CastMessage = _reflection.GeneratedProtocolMessageType('CastMessage', (_message.Message,), dict(
|
||||
DESCRIPTOR = _CASTMESSAGE,
|
||||
__module__ = 'cast_channel_pb2'
|
||||
# @@protoc_insertion_point(class_scope:extensions.core_api.cast_channel.CastMessage)
|
||||
))
|
||||
_sym_db.RegisterMessage(CastMessage)
|
||||
|
||||
AuthChallenge = _reflection.GeneratedProtocolMessageType('AuthChallenge', (_message.Message,), dict(
|
||||
DESCRIPTOR = _AUTHCHALLENGE,
|
||||
__module__ = 'cast_channel_pb2'
|
||||
# @@protoc_insertion_point(class_scope:extensions.core_api.cast_channel.AuthChallenge)
|
||||
))
|
||||
_sym_db.RegisterMessage(AuthChallenge)
|
||||
|
||||
AuthResponse = _reflection.GeneratedProtocolMessageType('AuthResponse', (_message.Message,), dict(
|
||||
DESCRIPTOR = _AUTHRESPONSE,
|
||||
__module__ = 'cast_channel_pb2'
|
||||
# @@protoc_insertion_point(class_scope:extensions.core_api.cast_channel.AuthResponse)
|
||||
))
|
||||
_sym_db.RegisterMessage(AuthResponse)
|
||||
|
||||
AuthError = _reflection.GeneratedProtocolMessageType('AuthError', (_message.Message,), dict(
|
||||
DESCRIPTOR = _AUTHERROR,
|
||||
__module__ = 'cast_channel_pb2'
|
||||
# @@protoc_insertion_point(class_scope:extensions.core_api.cast_channel.AuthError)
|
||||
))
|
||||
_sym_db.RegisterMessage(AuthError)
|
||||
|
||||
DeviceAuthMessage = _reflection.GeneratedProtocolMessageType('DeviceAuthMessage', (_message.Message,), dict(
|
||||
DESCRIPTOR = _DEVICEAUTHMESSAGE,
|
||||
__module__ = 'cast_channel_pb2'
|
||||
# @@protoc_insertion_point(class_scope:extensions.core_api.cast_channel.DeviceAuthMessage)
|
||||
))
|
||||
_sym_db.RegisterMessage(DeviceAuthMessage)
|
||||
|
||||
|
||||
DESCRIPTOR.has_options = True
|
||||
DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003'))
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
41
deps/pychromecast/config.py
vendored
Normal file
41
deps/pychromecast/config.py
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Data and methods to retrieve app specific configuration
|
||||
"""
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
APP_BACKDROP = "E8C28D3C"
|
||||
APP_YOUTUBE = "YouTube"
|
||||
APP_MEDIA_RECEIVER = "CC1AD845"
|
||||
APP_PLEX = "06ee44ee-e7e3-4249-83b6-f5d0b6f07f34_1"
|
||||
|
||||
|
||||
def get_possible_app_ids():
|
||||
""" Returns all possible app ids. """
|
||||
|
||||
try:
|
||||
req = requests.get(
|
||||
"https://clients3.google.com/cast/chromecast/device/baseconfig")
|
||||
data = json.loads(req.text[4:])
|
||||
|
||||
return [app['app_id'] for app in data['applications']] + \
|
||||
data["enabled_app_ids"]
|
||||
|
||||
except ValueError:
|
||||
# If json fails to parse
|
||||
return []
|
||||
|
||||
|
||||
def get_app_config(app_id):
|
||||
""" Get specific configuration for 'app_id'. """
|
||||
try:
|
||||
req = requests.get(
|
||||
("https://clients3.google.com/"
|
||||
"cast/chromecast/device/app?a={}").format(app_id))
|
||||
|
||||
return json.loads(req.text[4:]) if req.status_code == 200 else {}
|
||||
|
||||
except ValueError:
|
||||
# If json fails to parse
|
||||
return {}
|
||||
104
deps/pychromecast/controllers/__init__.py
vendored
Normal file
104
deps/pychromecast/controllers/__init__.py
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Provides controllers to handle specific namespaces in Chromecast communication.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ..error import UnsupportedNamespace, ControllerNotRegistered
|
||||
|
||||
|
||||
class BaseController(object):
|
||||
""" ABC for namespace controllers. """
|
||||
|
||||
def __init__(self, namespace, supporting_app_id=None,
|
||||
target_platform=False):
|
||||
"""
|
||||
Initialize the controller.
|
||||
|
||||
namespace: the namespace this controller will act on
|
||||
supporting_app_id: app to be launched if app is running with
|
||||
unsupported namespace.
|
||||
target_platform: set to True if you target the platform instead of
|
||||
current app.
|
||||
"""
|
||||
self.namespace = namespace
|
||||
self.supporting_app_id = supporting_app_id
|
||||
self.target_platform = target_platform
|
||||
|
||||
self._socket_client = None
|
||||
self._message_func = None
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
""" True if the controller is connected to a socket client and the
|
||||
Chromecast is running an app that supports this controller. """
|
||||
return (self._socket_client is not None and
|
||||
self.namespace in self._socket_client.app_namespaces)
|
||||
|
||||
def launch(self):
|
||||
""" If set, launches app related to the controller. """
|
||||
self._check_registered()
|
||||
|
||||
self._socket_client.receiver_controller.launch_app(
|
||||
self.supporting_app_id)
|
||||
|
||||
def registered(self, socket_client):
|
||||
""" Called when a controller is registered. """
|
||||
self._socket_client = socket_client
|
||||
|
||||
if self.target_platform:
|
||||
self._message_func = self._socket_client.send_platform_message
|
||||
else:
|
||||
self._message_func = self._socket_client.send_app_message
|
||||
|
||||
def channel_connected(self):
|
||||
""" Called when a channel has been openend that supports the
|
||||
namespace of this controller. """
|
||||
pass
|
||||
|
||||
def channel_disconnected(self):
|
||||
""" Called when a channel is disconnected. """
|
||||
pass
|
||||
|
||||
def send_message(self, data, inc_session_id=False,
|
||||
wait_for_response=False):
|
||||
"""
|
||||
Send a message on this namespace to the Chromecast.
|
||||
|
||||
Will raise a NotConnected exception if not connected.
|
||||
"""
|
||||
self._check_registered()
|
||||
|
||||
if not self.target_platform and \
|
||||
self.namespace not in self._socket_client.app_namespaces:
|
||||
if self.supporting_app_id is not None:
|
||||
self.launch()
|
||||
|
||||
else:
|
||||
raise UnsupportedNamespace(
|
||||
("Namespace {} is not supported by running"
|
||||
"application.").format(self.namespace))
|
||||
|
||||
return self._message_func(
|
||||
self.namespace, data, inc_session_id, wait_for_response)
|
||||
|
||||
# pylint: disable=unused-argument,no-self-use
|
||||
def receive_message(self, message, data):
|
||||
"""
|
||||
Called when a message is received that matches the namespace.
|
||||
Returns boolean indicating if message was handled.
|
||||
"""
|
||||
return False
|
||||
|
||||
def tear_down(self):
|
||||
""" Called when we are shutting down. """
|
||||
self._socket_client = None
|
||||
self._message_func = None
|
||||
|
||||
def _check_registered(self):
|
||||
""" Helper method to see if we are registered with a Cast object. """
|
||||
if self._socket_client is None:
|
||||
raise ControllerNotRegistered((
|
||||
"Trying to use the controller without it being registered "
|
||||
"with a Cast object."))
|
||||
BIN
deps/pychromecast/controllers/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/controllers/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/controllers/__pycache__/media.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/controllers/__pycache__/media.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/controllers/__pycache__/plex.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/controllers/__pycache__/plex.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/pychromecast/controllers/__pycache__/youtube.cpython-34.pyc
vendored
Normal file
BIN
deps/pychromecast/controllers/__pycache__/youtube.cpython-34.pyc
vendored
Normal file
Binary file not shown.
468
deps/pychromecast/controllers/media.py
vendored
Normal file
468
deps/pychromecast/controllers/media.py
vendored
Normal file
@@ -0,0 +1,468 @@
|
||||
"""
|
||||
Provides a controller for controlling the default media players
|
||||
on the Chromecast.
|
||||
"""
|
||||
from collections import namedtuple
|
||||
|
||||
from ..config import APP_MEDIA_RECEIVER
|
||||
from . import BaseController
|
||||
|
||||
STREAM_TYPE_UNKNOWN = "UNKNOWN"
|
||||
STREAM_TYPE_BUFFERED = "BUFFERED"
|
||||
STREAM_TYPE_LIVE = "LIFE"
|
||||
|
||||
MEDIA_PLAYER_STATE_PLAYING = "PLAYING"
|
||||
MEDIA_PLAYER_STATE_PAUSED = "PAUSED"
|
||||
MEDIA_PLAYER_STATE_IDLE = "IDLE"
|
||||
MEDIA_PLAYER_STATE_UNKNOWN = "UNKNOWN"
|
||||
|
||||
MESSAGE_TYPE = 'type'
|
||||
|
||||
TYPE_GET_STATUS = "GET_STATUS"
|
||||
TYPE_MEDIA_STATUS = "MEDIA_STATUS"
|
||||
TYPE_PLAY = "PLAY"
|
||||
TYPE_PAUSE = "PAUSE"
|
||||
TYPE_STOP = "STOP"
|
||||
TYPE_LOAD = "LOAD"
|
||||
TYPE_SEEK = "SEEK"
|
||||
TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO"
|
||||
|
||||
METADATA_TYPE_GENERIC = 0
|
||||
METADATA_TYPE_TVSHOW = 1
|
||||
METADATA_TYPE_MOVIE = 2
|
||||
METADATA_TYPE_MUSICTRACK = 3
|
||||
METADATA_TYPE_PHOTO = 4
|
||||
|
||||
CMD_SUPPORT_PAUSE = 1
|
||||
CMD_SUPPORT_SEEK = 2
|
||||
CMD_SUPPORT_STREAM_VOLUME = 4
|
||||
CMD_SUPPORT_STREAM_MUTE = 8
|
||||
CMD_SUPPORT_SKIP_FORWARD = 16
|
||||
CMD_SUPPORT_SKIP_BACKWARD = 32
|
||||
|
||||
|
||||
MediaImage = namedtuple('MediaImage', 'url height width')
|
||||
|
||||
|
||||
class MediaStatus(object):
|
||||
""" Class to hold the media status. """
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||
def __init__(self):
|
||||
self.current_time = 0
|
||||
self.content_id = None
|
||||
self.content_type = None
|
||||
self.duration = None
|
||||
self.stream_type = STREAM_TYPE_UNKNOWN
|
||||
self.idle_reason = None
|
||||
self.media_session_id = None
|
||||
self.playback_rate = 1
|
||||
self.player_state = MEDIA_PLAYER_STATE_UNKNOWN
|
||||
self.supported_media_commands = 0
|
||||
self.volume_level = 1
|
||||
self.volume_muted = False
|
||||
self.media_custom_data = {}
|
||||
self.media_metadata = {}
|
||||
self.subtitle_tracks = {}
|
||||
|
||||
@property
|
||||
def metadata_type(self):
|
||||
""" Type of meta data. """
|
||||
return self.media_metadata.get('metadataType')
|
||||
|
||||
@property
|
||||
def player_is_playing(self):
|
||||
""" Return True if player is PLAYING. """
|
||||
return self.player_state == MEDIA_PLAYER_STATE_PLAYING
|
||||
|
||||
@property
|
||||
def player_is_paused(self):
|
||||
""" Return True if player is PAUSED. """
|
||||
return self.player_state == MEDIA_PLAYER_STATE_PAUSED
|
||||
|
||||
@property
|
||||
def player_is_idle(self):
|
||||
""" Return True if player is IDLE. """
|
||||
return self.player_state == MEDIA_PLAYER_STATE_IDLE
|
||||
|
||||
@property
|
||||
def media_is_generic(self):
|
||||
""" Return True if media status represents generic media. """
|
||||
return self.metadata_type == METADATA_TYPE_GENERIC
|
||||
|
||||
@property
|
||||
def media_is_tvshow(self):
|
||||
""" Return True if media status represents a tv show. """
|
||||
return self.metadata_type == METADATA_TYPE_TVSHOW
|
||||
|
||||
@property
|
||||
def media_is_movie(self):
|
||||
""" Return True if media status represents a movie. """
|
||||
return self.metadata_type == METADATA_TYPE_MOVIE
|
||||
|
||||
@property
|
||||
def media_is_musictrack(self):
|
||||
""" Return True if media status represents a musictrack. """
|
||||
return self.metadata_type == METADATA_TYPE_MUSICTRACK
|
||||
|
||||
@property
|
||||
def media_is_photo(self):
|
||||
""" Return True if media status represents a photo. """
|
||||
return self.metadata_type == METADATA_TYPE_PHOTO
|
||||
|
||||
@property
|
||||
def stream_type_is_buffered(self):
|
||||
""" Return True if stream type is BUFFERED. """
|
||||
return self.stream_type == STREAM_TYPE_BUFFERED
|
||||
|
||||
@property
|
||||
def stream_type_is_live(self):
|
||||
""" Return True if stream type is LIVE. """
|
||||
return self.stream_type == STREAM_TYPE_LIVE
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""" Return title of media. """
|
||||
return self.media_metadata.get('title')
|
||||
|
||||
@property
|
||||
def series_title(self):
|
||||
""" Return series title if available. """
|
||||
return self.media_metadata.get('seriesTitle')
|
||||
|
||||
@property
|
||||
def season(self):
|
||||
""" Return season if available. """
|
||||
return self.media_metadata.get('season')
|
||||
|
||||
@property
|
||||
def episode(self):
|
||||
""" Return episode if available. """
|
||||
return self.media_metadata.get('episode')
|
||||
|
||||
@property
|
||||
def artist(self):
|
||||
""" Return artist if available. """
|
||||
return self.media_metadata.get('artist')
|
||||
|
||||
@property
|
||||
def album_name(self):
|
||||
""" Return album name if available. """
|
||||
return self.media_metadata.get('albumName')
|
||||
|
||||
@property
|
||||
def album_artist(self):
|
||||
""" Return album artist if available. """
|
||||
return self.media_metadata.get('albumArtist')
|
||||
|
||||
@property
|
||||
def track(self):
|
||||
""" Return track number if available. """
|
||||
return self.media_metadata.get('track')
|
||||
|
||||
@property
|
||||
def images(self):
|
||||
""" Return a list of MediaImage objects for this media. """
|
||||
return [
|
||||
MediaImage(item.get('url'), item.get('height'), item.get('width'))
|
||||
for item in self.media_metadata.get('images', [])
|
||||
]
|
||||
|
||||
@property
|
||||
def supports_pause(self):
|
||||
""" True if PAUSE is supported. """
|
||||
return bool(self.supported_media_commands & CMD_SUPPORT_PAUSE)
|
||||
|
||||
@property
|
||||
def supports_seek(self):
|
||||
""" True if SEEK is supported. """
|
||||
return bool(self.supported_media_commands & CMD_SUPPORT_SEEK)
|
||||
|
||||
@property
|
||||
def supports_stream_volume(self):
|
||||
""" True if STREAM_VOLUME is supported. """
|
||||
return bool(self.supported_media_commands & CMD_SUPPORT_STREAM_VOLUME)
|
||||
|
||||
@property
|
||||
def supports_stream_mute(self):
|
||||
""" True if STREAM_MUTE is supported. """
|
||||
return bool(self.supported_media_commands & CMD_SUPPORT_STREAM_MUTE)
|
||||
|
||||
@property
|
||||
def supports_skip_forward(self):
|
||||
""" True if SKIP_FORWARD is supported. """
|
||||
return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_FORWARD)
|
||||
|
||||
@property
|
||||
def supports_skip_backward(self):
|
||||
""" True if SKIP_BACKWARD is supported. """
|
||||
return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_BACKWARD)
|
||||
|
||||
def update(self, data):
|
||||
""" New data will only contain the changed attributes. """
|
||||
if len(data.get('status', [])) == 0:
|
||||
return
|
||||
|
||||
status_data = data['status'][0]
|
||||
media_data = status_data.get('media') or {}
|
||||
volume_data = status_data.get('volume', {})
|
||||
|
||||
self.current_time = status_data.get('currentTime', self.current_time)
|
||||
self.content_id = media_data.get('contentId', self.content_id)
|
||||
self.content_type = media_data.get('contentType', self.content_type)
|
||||
self.duration = media_data.get('duration', self.duration)
|
||||
self.stream_type = media_data.get('streamType', self.stream_type)
|
||||
self.idle_reason = status_data.get('idleReason', self.idle_reason)
|
||||
self.media_session_id = status_data.get(
|
||||
'mediaSessionId', self.media_session_id)
|
||||
self.playback_rate = status_data.get(
|
||||
'playbackRate', self.playback_rate)
|
||||
self.player_state = status_data.get('playerState', self.player_state)
|
||||
self.supported_media_commands = status_data.get(
|
||||
'supportedMediaCommands', self.supported_media_commands)
|
||||
self.volume_level = volume_data.get('level', self.volume_level)
|
||||
self.volume_muted = volume_data.get('muted', self.volume_muted)
|
||||
self.media_custom_data = media_data.get(
|
||||
'customData', self.media_custom_data)
|
||||
self.media_metadata = media_data.get('metadata', self.media_metadata)
|
||||
self.subtitle_tracks = media_data.get('tracks', self.subtitle_tracks)
|
||||
|
||||
def __repr__(self):
|
||||
info = {
|
||||
'metadata_type': self.metadata_type,
|
||||
'title': self.title,
|
||||
'series_title': self.series_title,
|
||||
'season': self.season,
|
||||
'episode': self.episode,
|
||||
'artist': self.artist,
|
||||
'album_name': self.album_name,
|
||||
'album_artist': self.album_artist,
|
||||
'track': self.track,
|
||||
'subtitle_tracks': self.subtitle_tracks,
|
||||
'images': self.images,
|
||||
'supports_pause': self.supports_pause,
|
||||
'supports_seek': self.supports_seek,
|
||||
'supports_stream_volume': self.supports_stream_volume,
|
||||
'supports_stream_mute': self.supports_stream_mute,
|
||||
'supports_skip_forward': self.supports_skip_forward,
|
||||
'supports_skip_backward': self.supports_skip_backward,
|
||||
}
|
||||
info.update(self.__dict__)
|
||||
return '<MediaStatus {}>'.format(info)
|
||||
|
||||
|
||||
class MediaController(BaseController):
|
||||
""" Controller to interact with Google media namespace. """
|
||||
|
||||
def __init__(self):
|
||||
super(MediaController, self).__init__(
|
||||
"urn:x-cast:com.google.cast.media")
|
||||
|
||||
self.media_session_id = 0
|
||||
self.status = MediaStatus()
|
||||
self.app_id = APP_MEDIA_RECEIVER
|
||||
self._status_listeners = []
|
||||
|
||||
def channel_connected(self):
|
||||
""" Called when media channel is connected. Will update status. """
|
||||
self.update_status()
|
||||
|
||||
def channel_disconnected(self):
|
||||
""" Called when a media channel is disconnected. Will erase status. """
|
||||
self.status = MediaStatus()
|
||||
self._fire_status_changed()
|
||||
|
||||
def receive_message(self, message, data):
|
||||
""" Called when a media message is received. """
|
||||
if data[MESSAGE_TYPE] == TYPE_MEDIA_STATUS:
|
||||
self._process_media_status(data)
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
def register_status_listener(self, listener):
|
||||
""" Register a listener for new media statusses. A new status will
|
||||
call listener.new_media_status(status) """
|
||||
self._status_listeners.append(listener)
|
||||
|
||||
def update_status(self, blocking=False):
|
||||
""" Send message to update the status. """
|
||||
self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS},
|
||||
wait_for_response=blocking)
|
||||
|
||||
def _send_command(self, command):
|
||||
""" Send a command to the Chromecast on media channel. """
|
||||
if self.status is None or self.status.media_session_id is None:
|
||||
return
|
||||
|
||||
command['mediaSessionId'] = self.status.media_session_id
|
||||
|
||||
self.send_message(command, inc_session_id=True)
|
||||
|
||||
@property
|
||||
def is_playing(self):
|
||||
""" Deprecated as of June 8, 2015. Use self.status.player_is_playing.
|
||||
Returns if the Chromecast is playing. """
|
||||
return self.status is not None and self.status.player_is_playing
|
||||
|
||||
@property
|
||||
def is_paused(self):
|
||||
""" Deprecated as of June 8, 2015. Use self.status.player_is_paused.
|
||||
Returns if the Chromecast is paused. """
|
||||
return self.status is not None and self.status.player_is_paused
|
||||
|
||||
@property
|
||||
def is_idle(self):
|
||||
""" Deprecated as of June 8, 2015. Use self.status.player_is_idle.
|
||||
Returns if the Chromecast is idle on a media supported app. """
|
||||
return self.status is not None and self.status.player_is_idle
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""" Deprecated as of June 8, 2015. Use self.status.title.
|
||||
Return title of the current playing item. """
|
||||
return None if not self.status else self.status.title
|
||||
|
||||
@property
|
||||
def thumbnail(self):
|
||||
""" Deprecated as of June 8, 2015. Use self.status.images.
|
||||
Return thumbnail url of current playing item. """
|
||||
if not self.status:
|
||||
return None
|
||||
|
||||
images = self.status.images
|
||||
|
||||
return images[0].url if images and len(images) > 0 else None
|
||||
|
||||
def play(self):
|
||||
""" Send the PLAY command. """
|
||||
self._send_command({MESSAGE_TYPE: TYPE_PLAY})
|
||||
|
||||
def pause(self):
|
||||
""" Send the PAUSE command. """
|
||||
self._send_command({MESSAGE_TYPE: TYPE_PAUSE})
|
||||
|
||||
def stop(self):
|
||||
""" Send the STOP command. """
|
||||
self._send_command({MESSAGE_TYPE: TYPE_STOP})
|
||||
|
||||
def rewind(self):
|
||||
""" Starts playing the media from the beginning. """
|
||||
self.seek(0)
|
||||
|
||||
def skip(self):
|
||||
""" Skips rest of the media. Values less then -5 behaved flaky. """
|
||||
self.seek(int(self.status.duration)-5)
|
||||
|
||||
def seek(self, position):
|
||||
""" Seek the media to a specific location. """
|
||||
self._send_command({MESSAGE_TYPE: TYPE_SEEK,
|
||||
"currentTime": position,
|
||||
"resumeState": "PLAYBACK_START"})
|
||||
|
||||
def enable_subtitle(self, track_id):
|
||||
""" Enable specific text track. """
|
||||
self._send_command({
|
||||
MESSAGE_TYPE: TYPE_EDIT_TRACKS_INFO,
|
||||
"activeTrackIds": [track_id]
|
||||
})
|
||||
|
||||
def disable_subtitle(self):
|
||||
""" Disable subtitle. """
|
||||
self._send_command({
|
||||
MESSAGE_TYPE: TYPE_EDIT_TRACKS_INFO,
|
||||
"activeTrackIds": []
|
||||
})
|
||||
|
||||
def _process_media_status(self, data):
|
||||
""" Processes a STATUS message. """
|
||||
self.status.update(data)
|
||||
|
||||
self.logger.debug("Media:Received status %s", data)
|
||||
self._fire_status_changed()
|
||||
|
||||
def _fire_status_changed(self):
|
||||
""" Tells listeners of a changed status. """
|
||||
for listener in self._status_listeners:
|
||||
try:
|
||||
listener.new_media_status(self.status)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def play_media(self, url, content_type, title=None, thumb=None,
|
||||
current_time=0, autoplay=True,
|
||||
stream_type=STREAM_TYPE_BUFFERED,
|
||||
metadata=None, subtitles=None, subtitles_lang='en-US',
|
||||
subtitles_mime='text/vtt', subtitle_id=1):
|
||||
"""
|
||||
Plays media on the Chromecast. Start default media receiver if not
|
||||
already started.
|
||||
|
||||
Parameters:
|
||||
url: str - url of the media.
|
||||
content_type: str - mime type. Example: 'video/mp4'.
|
||||
title: str - title of the media.
|
||||
thumb: str - thumbnail image url.
|
||||
current_time: float - seconds from the beginning of the media
|
||||
to start playback.
|
||||
autoplay: bool - whether the media will automatically play.
|
||||
stream_type: str - describes the type of media artifact as one of the
|
||||
following: "NONE", "BUFFERED", "LIVE".
|
||||
subtitles: str - url of subtitle file to be shown on chromecast.
|
||||
subtitles_lang: str - language for subtitles.
|
||||
subtitles_mime: str - mimetype of subtitles.
|
||||
subtitle_id: int - id of subtitle to be loaded.
|
||||
metadata: dict - media metadata object, one of the following:
|
||||
GenericMediaMetadata, MovieMediaMetadata, TvShowMediaMetadata,
|
||||
MusicTrackMediaMetadata, PhotoMediaMetadata.
|
||||
|
||||
Docs:
|
||||
https://developers.google.com/cast/docs/reference/messages#MediaData
|
||||
"""
|
||||
|
||||
self._socket_client.receiver_controller.launch_app(self.app_id)
|
||||
|
||||
msg = {
|
||||
'media': {
|
||||
'contentId': url,
|
||||
'streamType': stream_type,
|
||||
'contentType': content_type,
|
||||
'metadata': metadata or {}
|
||||
},
|
||||
MESSAGE_TYPE: TYPE_LOAD,
|
||||
'currentTime': current_time,
|
||||
'autoplay': autoplay,
|
||||
'customData': {}
|
||||
}
|
||||
|
||||
if title:
|
||||
msg['media']['metadata']['title'] = title
|
||||
|
||||
if thumb:
|
||||
msg['media']['metadata']['thumb'] = thumb
|
||||
|
||||
if 'images' not in msg['media']['metadata']:
|
||||
msg['media']['metadata']['images'] = []
|
||||
|
||||
msg['media']['metadata']['images'].append({'url': thumb})
|
||||
if subtitles:
|
||||
sub_msg = {
|
||||
'trackId': subtitle_id,
|
||||
'trackContentId': subtitles,
|
||||
'language': subtitles_lang,
|
||||
'subtype': 'SUBTITLES',
|
||||
'type': 'TEXT',
|
||||
'ttrackContentType': subtitles_mime,
|
||||
'name': "{} - {} Subtitle".format(subtitles_lang, subtitle_id)
|
||||
}
|
||||
msg['media']['tracks'] = sub_msg
|
||||
self.send_message(msg, inc_session_id=True)
|
||||
|
||||
def tear_down(self):
|
||||
""" Called when controller is destroyed. """
|
||||
super(MediaController, self).tear_down()
|
||||
|
||||
self._status_listeners[:] = []
|
||||
30
deps/pychromecast/controllers/plex.py
vendored
Normal file
30
deps/pychromecast/controllers/plex.py
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Controller to interface with the Plex-app.
|
||||
"""
|
||||
from . import BaseController
|
||||
|
||||
MESSAGE_TYPE = 'type'
|
||||
|
||||
TYPE_PLAY = "PLAY"
|
||||
TYPE_PAUSE = "PAUSE"
|
||||
TYPE_STOP = "STOP"
|
||||
|
||||
|
||||
class PlexController(BaseController):
|
||||
""" Controller to interact with Plex namespace. """
|
||||
|
||||
def __init__(self):
|
||||
super(PlexController, self).__init__(
|
||||
"urn:x-cast:plex", "9AC194DC")
|
||||
|
||||
def stop(self):
|
||||
""" Send stop command. """
|
||||
self.send_message({MESSAGE_TYPE: TYPE_STOP})
|
||||
|
||||
def pause(self):
|
||||
""" Send pause command. """
|
||||
self.send_message({MESSAGE_TYPE: TYPE_PAUSE})
|
||||
|
||||
def play(self):
|
||||
""" Send play command. """
|
||||
self.send_message({MESSAGE_TYPE: TYPE_PLAY})
|
||||
52
deps/pychromecast/controllers/youtube.py
vendored
Normal file
52
deps/pychromecast/controllers/youtube.py
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Controller to interface with the YouTube-app.
|
||||
|
||||
Use the media controller to play, pause etc.
|
||||
"""
|
||||
from . import BaseController
|
||||
|
||||
MESSAGE_TYPE = "type"
|
||||
TYPE_STATUS = "mdxSessionStatus"
|
||||
ATTR_SCREEN_ID = "screenId"
|
||||
|
||||
|
||||
class YouTubeController(BaseController):
|
||||
""" Controller to interact with Youtube namespace. """
|
||||
|
||||
def __init__(self):
|
||||
super(YouTubeController, self).__init__(
|
||||
"urn:x-cast:com.google.youtube.mdx", "233637DE")
|
||||
|
||||
self.screen_id = None
|
||||
|
||||
def receive_message(self, message, data):
|
||||
""" Called when a media message is received. """
|
||||
if data[MESSAGE_TYPE] == TYPE_STATUS:
|
||||
self._process_status(data.get('data'))
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
def play_video(self, youtube_id):
|
||||
"""
|
||||
Starts playing a video in the YouTube app.
|
||||
|
||||
Only works if there is no video playing.
|
||||
"""
|
||||
self.launch()
|
||||
|
||||
msg = {
|
||||
"type": "flingVideo",
|
||||
"data": {
|
||||
"currentTime": 0,
|
||||
"videoId": youtube_id
|
||||
}
|
||||
}
|
||||
|
||||
self.send_message(msg, inc_session_id=True)
|
||||
|
||||
def _process_status(self, status):
|
||||
""" Process latest status update. """
|
||||
self.screen_id = status.get(ATTR_SCREEN_ID)
|
||||
111
deps/pychromecast/dial.py
vendored
Normal file
111
deps/pychromecast/dial.py
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Implements the DIAL-protocol to communicate with the Chromecast
|
||||
"""
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import namedtuple
|
||||
from uuid import UUID
|
||||
|
||||
import requests
|
||||
import six
|
||||
|
||||
XML_NS_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}"
|
||||
|
||||
FORMAT_BASE_URL = "http://{}:8008"
|
||||
|
||||
CC_SESSION = requests.Session()
|
||||
CC_SESSION.headers['content-type'] = 'application/json'
|
||||
|
||||
# Regular chromecast, supports video/audio
|
||||
CAST_TYPE_CHROMECAST = 'cast'
|
||||
# Cast Audio device, supports only audio
|
||||
CAST_TYPE_AUDIO = 'audio'
|
||||
# Cast Audio group device, supports only audio
|
||||
CAST_TYPE_GROUP = 'group'
|
||||
|
||||
CAST_TYPES = {
|
||||
u'chromecast': CAST_TYPE_CHROMECAST,
|
||||
u'chromecast audio': CAST_TYPE_AUDIO,
|
||||
u'google cast group': CAST_TYPE_GROUP,
|
||||
}
|
||||
|
||||
|
||||
def reboot(host):
|
||||
""" Reboots the chromecast. """
|
||||
CC_SESSION.post(FORMAT_BASE_URL.format(host) + "/setup/reboot",
|
||||
data='{"params":"now"}', timeout=10)
|
||||
|
||||
|
||||
def get_device_status(host):
|
||||
"""
|
||||
:param host: Hostname or ip to fetch status from
|
||||
:type host: str
|
||||
:return: The device status as a named tuple.
|
||||
:rtype: pychromecast.dial.DeviceStatus or None
|
||||
"""
|
||||
|
||||
try:
|
||||
req = CC_SESSION.get(
|
||||
FORMAT_BASE_URL.format(host) + "/ssdp/device-desc.xml",
|
||||
timeout=10)
|
||||
|
||||
# The Requests library will fall back to guessing the encoding in case
|
||||
# no encoding is specified in the response headers - which is the case
|
||||
# for the Chromecast.
|
||||
# The standard mandates utf-8 encoding, let's fall back to that instead
|
||||
# if no encoding is provided, since the autodetection does not always
|
||||
# provide correct results.
|
||||
if req.encoding is None:
|
||||
req.encoding = 'utf-8'
|
||||
|
||||
status_el = ET.fromstring(req.text.encode("UTF-8"))
|
||||
|
||||
device_info_el = status_el.find(XML_NS_UPNP_DEVICE + "device")
|
||||
api_version_el = status_el.find(XML_NS_UPNP_DEVICE + "specVersion")
|
||||
|
||||
friendly_name = _read_xml_element(device_info_el, XML_NS_UPNP_DEVICE,
|
||||
"friendlyName", "Unknown Chromecast")
|
||||
model_name = _read_xml_element(device_info_el, XML_NS_UPNP_DEVICE,
|
||||
"modelName", "Unknown model name")
|
||||
manufacturer = _read_xml_element(device_info_el, XML_NS_UPNP_DEVICE,
|
||||
"manufacturer",
|
||||
"Unknown manufacturer")
|
||||
udn = _read_xml_element(device_info_el, XML_NS_UPNP_DEVICE,
|
||||
"UDN",
|
||||
None)
|
||||
|
||||
api_version = (int(_read_xml_element(api_version_el,
|
||||
XML_NS_UPNP_DEVICE, "major", -1)),
|
||||
int(_read_xml_element(api_version_el,
|
||||
XML_NS_UPNP_DEVICE, "minor", -1)))
|
||||
|
||||
cast_type = CAST_TYPES.get(model_name.lower(),
|
||||
CAST_TYPE_CHROMECAST)
|
||||
|
||||
uuid = None
|
||||
if udn and udn.startswith('uuid:'):
|
||||
uuid = UUID(udn[len('uuid:'):].replace("-", ""))
|
||||
|
||||
return DeviceStatus(friendly_name, model_name, manufacturer,
|
||||
api_version, uuid, cast_type)
|
||||
|
||||
except (requests.exceptions.RequestException, ET.ParseError):
|
||||
return None
|
||||
|
||||
|
||||
def _read_xml_element(element, xml_ns, tag_name, default=""):
|
||||
""" Helper method to read text from an element. """
|
||||
try:
|
||||
text = element.find(xml_ns + tag_name).text
|
||||
if isinstance(text, six.text_type):
|
||||
return text
|
||||
else:
|
||||
return text.decode('utf-8')
|
||||
|
||||
except AttributeError:
|
||||
return default
|
||||
|
||||
|
||||
DeviceStatus = namedtuple("DeviceStatus",
|
||||
["friendly_name", "model_name",
|
||||
"manufacturer", "api_version",
|
||||
"uuid", "cast_type"])
|
||||
90
deps/pychromecast/discovery.py
vendored
Normal file
90
deps/pychromecast/discovery.py
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Discovers Chromecasts on the network using mDNS/zeroconf."""
|
||||
import time
|
||||
from uuid import UUID
|
||||
|
||||
import six
|
||||
from zeroconf import ServiceBrowser, Zeroconf
|
||||
|
||||
DISCOVER_TIMEOUT = 5
|
||||
|
||||
|
||||
class CastListener(object):
|
||||
"""Zeroconf Cast Services collection."""
|
||||
def __init__(self):
|
||||
self.services = {}
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
"""Number of discovered cast services."""
|
||||
return len(self.services)
|
||||
|
||||
@property
|
||||
def devices(self):
|
||||
"""List of tuples (ip, host) for each discovered device."""
|
||||
return list(self.services.values())
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def remove_service(self, zconf, typ, name):
|
||||
""" Remove a service from the collection. """
|
||||
self.services.pop(name, None)
|
||||
|
||||
def add_service(self, zconf, typ, name):
|
||||
""" Add a service to the collection. """
|
||||
service = None
|
||||
tries = 0
|
||||
while service is None and tries < 4:
|
||||
try:
|
||||
service = zconf.get_service_info(typ, name)
|
||||
except IOError:
|
||||
# If the zerconf fails to receive the necesarry data we abort
|
||||
# adding the service
|
||||
break
|
||||
tries += 1
|
||||
|
||||
if not service:
|
||||
return
|
||||
|
||||
def get_value(key):
|
||||
"""Retrieve value and decode for Python 2/3."""
|
||||
value = service.properties.get(key.encode('utf-8'))
|
||||
|
||||
if value is None or isinstance(value, six.text_type):
|
||||
return value
|
||||
return value.decode('utf-8')
|
||||
|
||||
ips = zconf.cache.entries_with_name(service.server.lower())
|
||||
host = repr(ips[0]) if ips else service.server
|
||||
|
||||
model_name = get_value('md')
|
||||
uuid = get_value('id')
|
||||
friendly_name = get_value('fn')
|
||||
|
||||
if uuid:
|
||||
uuid = UUID(uuid)
|
||||
|
||||
self.services[name] = (host, service.port, uuid, model_name,
|
||||
friendly_name)
|
||||
|
||||
|
||||
def discover_chromecasts(max_devices=None, timeout=DISCOVER_TIMEOUT):
|
||||
""" Discover chromecasts on the network. """
|
||||
try:
|
||||
zconf = Zeroconf()
|
||||
listener = CastListener()
|
||||
browser = ServiceBrowser(zconf, "_googlecast._tcp.local.", listener)
|
||||
|
||||
if max_devices is None:
|
||||
time.sleep(timeout)
|
||||
return listener.devices
|
||||
|
||||
else:
|
||||
start = time.time()
|
||||
|
||||
while (time.time() - start < timeout and
|
||||
listener.count < max_devices):
|
||||
time.sleep(.1)
|
||||
|
||||
return listener.devices
|
||||
finally:
|
||||
browser.cancel()
|
||||
zconf.close()
|
||||
63
deps/pychromecast/error.py
vendored
Normal file
63
deps/pychromecast/error.py
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Errors to be used by PyChromecast.
|
||||
"""
|
||||
|
||||
|
||||
class PyChromecastError(Exception):
|
||||
""" Base error for PyChromecast. """
|
||||
pass
|
||||
|
||||
|
||||
class NoChromecastFoundError(PyChromecastError):
|
||||
"""
|
||||
When a command has to auto-discover a Chromecast and cannot find one.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MultipleChromecastsFoundError(PyChromecastError):
|
||||
"""
|
||||
When getting a singular chromecast results in getting multiple chromecasts.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ChromecastConnectionError(PyChromecastError):
|
||||
""" When a connection error occurs within PyChromecast. """
|
||||
pass
|
||||
|
||||
|
||||
class LaunchError(PyChromecastError):
|
||||
""" When an app fails to launch. """
|
||||
pass
|
||||
|
||||
|
||||
class PyChromecastStopped(PyChromecastError):
|
||||
""" Raised when a command is invoked while the Chromecast's socket_client
|
||||
is stopped.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NotConnected(PyChromecastError):
|
||||
"""
|
||||
Raised when a command is invoked while not connected to a Chromecast.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedNamespace(PyChromecastError):
|
||||
"""
|
||||
Raised when trying to send a message with a namespace that is not
|
||||
supported by the current running app.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ControllerNotRegistered(PyChromecastError):
|
||||
"""
|
||||
Raised when trying to interact with a controller while it is
|
||||
not registered with a ChromeCast object.
|
||||
"""
|
||||
pass
|
||||
809
deps/pychromecast/logging_pb2.py
vendored
Normal file
809
deps/pychromecast/logging_pb2.py
vendored
Normal file
File diff suppressed because one or more lines are too long
945
deps/pychromecast/socket_client.py
vendored
Normal file
945
deps/pychromecast/socket_client.py
vendored
Normal file
@@ -0,0 +1,945 @@
|
||||
"""
|
||||
Module to interact with the ChromeCast via protobuf-over-socket.
|
||||
|
||||
Big thanks goes out to Fred Clift <fred@clift.org> who build the first
|
||||
version of this code: https://github.com/minektur/chromecast-python-poc.
|
||||
Without him this would not have been possible.
|
||||
"""
|
||||
# Pylint does not understand the protobuf objects correctly
|
||||
# pylint: disable=no-member
|
||||
|
||||
import logging
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from struct import pack, unpack
|
||||
import sys
|
||||
|
||||
from . import cast_channel_pb2
|
||||
from .dial import CAST_TYPE_CHROMECAST, CAST_TYPE_AUDIO, CAST_TYPE_GROUP
|
||||
from .controllers import BaseController
|
||||
from .controllers.media import MediaController
|
||||
from .error import (
|
||||
ChromecastConnectionError,
|
||||
UnsupportedNamespace,
|
||||
NotConnected,
|
||||
PyChromecastStopped,
|
||||
LaunchError,
|
||||
)
|
||||
|
||||
NS_CONNECTION = 'urn:x-cast:com.google.cast.tp.connection'
|
||||
NS_RECEIVER = 'urn:x-cast:com.google.cast.receiver'
|
||||
NS_HEARTBEAT = 'urn:x-cast:com.google.cast.tp.heartbeat'
|
||||
|
||||
PLATFORM_DESTINATION_ID = "receiver-0"
|
||||
|
||||
MESSAGE_TYPE = 'type'
|
||||
TYPE_PING = "PING"
|
||||
TYPE_RECEIVER_STATUS = "RECEIVER_STATUS"
|
||||
TYPE_PONG = "PONG"
|
||||
TYPE_CONNECT = "CONNECT"
|
||||
TYPE_CLOSE = "CLOSE"
|
||||
TYPE_GET_STATUS = "GET_STATUS"
|
||||
TYPE_LAUNCH = "LAUNCH"
|
||||
TYPE_LAUNCH_ERROR = "LAUNCH_ERROR"
|
||||
TYPE_LOAD = "LOAD"
|
||||
|
||||
# The socket connection is being setup
|
||||
CONNECTION_STATUS_CONNECTING = "CONNECTING"
|
||||
# The socket connection was complete
|
||||
CONNECTION_STATUS_CONNECTED = "CONNECTED"
|
||||
# The socket connection has been disconnected
|
||||
CONNECTION_STATUS_DISCONNECTED = "DISCONNECTED"
|
||||
# Connecting to socket failed (after a CONNECTION_STATUS_CONNECTING)
|
||||
CONNECTION_STATUS_FAILED = "FAILED"
|
||||
# The socket connection was lost and needs to be retried
|
||||
CONNECTION_STATUS_LOST = "LOST"
|
||||
|
||||
APP_ID = 'appId'
|
||||
REQUEST_ID = "requestId"
|
||||
SESSION_ID = "sessionId"
|
||||
ERROR_REASON = 'reason'
|
||||
|
||||
HB_PING_TIME = 10
|
||||
HB_PONG_TIME = 10
|
||||
POLL_TIME = 5
|
||||
TIMEOUT_TIME = 30
|
||||
RETRY_TIME = 5
|
||||
|
||||
|
||||
class InterruptLoop(Exception):
|
||||
""" The chromecast has been manually stopped. """
|
||||
pass
|
||||
|
||||
|
||||
def _json_from_message(message):
|
||||
""" Parses a PB2 message into JSON format. """
|
||||
return json.loads(message.payload_utf8)
|
||||
|
||||
|
||||
def _message_to_string(message, data=None):
|
||||
""" Gives a string representation of a PB2 message. """
|
||||
if data is None:
|
||||
data = _json_from_message(message)
|
||||
|
||||
return "Message {} from {} to {}: {}".format(
|
||||
message.namespace, message.source_id, message.destination_id, data)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
def _json_to_payload(data):
|
||||
""" Encodes a python value into JSON format. """
|
||||
return json.dumps(data, ensure_ascii=False).encode("utf8")
|
||||
else:
|
||||
def _json_to_payload(data):
|
||||
""" Encodes a python value into JSON format. """
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
def _is_ssl_timeout(exc):
|
||||
""" Returns True if the exception is for an SSL timeout """
|
||||
return exc.message in ("The handshake operation timed out",
|
||||
"The write operation timed out",
|
||||
"The read operation timed out")
|
||||
|
||||
|
||||
NetworkAddress = namedtuple('NetworkAddress',
|
||||
['address', 'port'])
|
||||
|
||||
|
||||
ConnectionStatus = namedtuple('ConnectionStatus',
|
||||
['status', 'address'])
|
||||
|
||||
|
||||
CastStatus = namedtuple('CastStatus',
|
||||
['is_active_input', 'is_stand_by', 'volume_level',
|
||||
'volume_muted', 'app_id', 'display_name',
|
||||
'namespaces', 'session_id', 'transport_id',
|
||||
'status_text'])
|
||||
|
||||
|
||||
LaunchFailure = namedtuple('LaunchStatus',
|
||||
['reason', 'app_id', 'request_id'])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class SocketClient(threading.Thread):
|
||||
"""
|
||||
Class to interact with a Chromecast through a socket.
|
||||
|
||||
:param port: The port to use when connecting to the device, set to None to
|
||||
use the default of 8009. Special devices such as Cast Groups
|
||||
may return a different port number so we need to use that.
|
||||
:param cast_type: The type of chromecast to connect to, see
|
||||
dial.CAST_TYPE_* for types.
|
||||
:param tries: Number of retries to perform if the connection fails.
|
||||
None for inifinite retries.
|
||||
:param retry_wait: A floating point number specifying how many seconds to
|
||||
wait between each retry. None means to use the default
|
||||
which is 5 seconds.
|
||||
"""
|
||||
|
||||
def __init__(self, host, port=None, cast_type=CAST_TYPE_CHROMECAST,
|
||||
**kwargs):
|
||||
tries = kwargs.pop('tries', None)
|
||||
timeout = kwargs.pop('timeout', None)
|
||||
retry_wait = kwargs.pop('retry_wait', None)
|
||||
|
||||
super(SocketClient, self).__init__()
|
||||
|
||||
self.daemon = True
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
self._force_recon = False
|
||||
|
||||
self.cast_type = cast_type
|
||||
self.tries = tries
|
||||
self.timeout = timeout or TIMEOUT_TIME
|
||||
self.retry_wait = retry_wait or RETRY_TIME
|
||||
self.host = host
|
||||
self.port = port or 8009
|
||||
|
||||
self.source_id = "sender-0"
|
||||
self.stop = threading.Event()
|
||||
|
||||
self.app_namespaces = []
|
||||
self.destination_id = None
|
||||
self.session_id = None
|
||||
self._request_id = 0
|
||||
# dict mapping requestId on threading.Event objects
|
||||
self._request_callbacks = {}
|
||||
self._open_channels = []
|
||||
|
||||
self.connecting = True
|
||||
self.socket = None
|
||||
|
||||
# dict mapping namespace on Controller objects
|
||||
self._handlers = {}
|
||||
self._connection_listeners = []
|
||||
|
||||
self.receiver_controller = ReceiverController(cast_type)
|
||||
self.media_controller = MediaController()
|
||||
self.heartbeat_controller = HeartbeatController()
|
||||
|
||||
self.register_handler(self.heartbeat_controller)
|
||||
self.register_handler(ConnectionController())
|
||||
self.register_handler(self.receiver_controller)
|
||||
self.register_handler(self.media_controller)
|
||||
|
||||
self.receiver_controller.register_status_listener(self)
|
||||
|
||||
try:
|
||||
self.initialize_connection()
|
||||
except ChromecastConnectionError:
|
||||
self._report_connection_status(
|
||||
ConnectionStatus(CONNECTION_STATUS_DISCONNECTED,
|
||||
NetworkAddress(self.host, self.port)))
|
||||
raise
|
||||
|
||||
def initialize_connection(self):
|
||||
"""Initialize a socket to a Chromecast, retrying as necessary."""
|
||||
tries = self.tries
|
||||
|
||||
if self.socket is not None:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
# Make sure nobody is blocking.
|
||||
for callback in self._request_callbacks.values():
|
||||
callback['event'].set()
|
||||
|
||||
self.app_namespaces = []
|
||||
self.destination_id = None
|
||||
self.session_id = None
|
||||
self._request_id = 0
|
||||
self._request_callbacks = {}
|
||||
self._open_channels = []
|
||||
|
||||
self.connecting = True
|
||||
retry_log_fun = self.logger.exception
|
||||
|
||||
while not self.stop.is_set() and (tries is None or tries > 0):
|
||||
try:
|
||||
self.socket = ssl.wrap_socket(socket.socket())
|
||||
self.socket.settimeout(self.timeout)
|
||||
self._report_connection_status(
|
||||
ConnectionStatus(CONNECTION_STATUS_CONNECTING,
|
||||
NetworkAddress(self.host, self.port)))
|
||||
self.socket.connect((self.host, self.port))
|
||||
self.connecting = False
|
||||
self._force_recon = False
|
||||
self._report_connection_status(
|
||||
ConnectionStatus(CONNECTION_STATUS_CONNECTED,
|
||||
NetworkAddress(self.host, self.port)))
|
||||
self.receiver_controller.update_status()
|
||||
self.heartbeat_controller.ping()
|
||||
self.heartbeat_controller.reset()
|
||||
|
||||
self.logger.debug("Connected!")
|
||||
break
|
||||
except socket.error:
|
||||
self.connecting = True
|
||||
if self.stop.is_set():
|
||||
self.logger.exception(
|
||||
"Failed to connect, aborting due to stop signal.")
|
||||
raise ChromecastConnectionError("Failed to connect")
|
||||
|
||||
self._report_connection_status(
|
||||
ConnectionStatus(CONNECTION_STATUS_FAILED,
|
||||
NetworkAddress(self.host, self.port)))
|
||||
retry_log_fun("Failed to connect, retrying in %fs",
|
||||
self.retry_wait)
|
||||
retry_log_fun = self.logger.debug
|
||||
|
||||
time.sleep(self.retry_wait)
|
||||
if tries:
|
||||
tries -= 1
|
||||
else:
|
||||
self.stop.set()
|
||||
self.logger.error("Failed to connect. No retries.")
|
||||
raise ChromecastConnectionError("Failed to connect")
|
||||
|
||||
def disconnect(self):
|
||||
""" Disconnect socket connection to Chromecast device """
|
||||
self.stop.set()
|
||||
|
||||
def register_handler(self, handler):
|
||||
""" Register a new namespace handler. """
|
||||
self._handlers[handler.namespace] = handler
|
||||
|
||||
handler.registered(self)
|
||||
|
||||
def new_cast_status(self, cast_status):
|
||||
""" Called when a new cast status has been received. """
|
||||
new_channel = self.destination_id != cast_status.transport_id
|
||||
|
||||
if new_channel:
|
||||
self._disconnect_channel(self.destination_id)
|
||||
|
||||
self.app_namespaces = cast_status.namespaces
|
||||
self.destination_id = cast_status.transport_id
|
||||
self.session_id = cast_status.session_id
|
||||
|
||||
if new_channel:
|
||||
# If any of the namespaces of the new app are supported
|
||||
# we will automatically connect to it to receive updates
|
||||
for namespace in self.app_namespaces:
|
||||
if namespace in self._handlers:
|
||||
self._ensure_channel_connected(self.destination_id)
|
||||
self._handlers[namespace].channel_connected()
|
||||
|
||||
def _gen_request_id(self):
|
||||
""" Generates a unique request id. """
|
||||
self._request_id += 1
|
||||
|
||||
return self._request_id
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
"""
|
||||
Returns True if the client is connected, False if it is stopped
|
||||
(or trying to connect).
|
||||
"""
|
||||
return not self.connecting
|
||||
|
||||
@property
|
||||
def is_stopped(self):
|
||||
"""
|
||||
Returns True if the connection has been stopped, False if it is
|
||||
running.
|
||||
"""
|
||||
return self.stop.is_set()
|
||||
|
||||
def run(self):
|
||||
""" Start polling the socket. """
|
||||
# pylint: disable=too-many-branches
|
||||
self.heartbeat_controller.reset()
|
||||
self._force_recon = False
|
||||
while not self.stop.is_set():
|
||||
|
||||
try:
|
||||
if not self._check_connection():
|
||||
continue
|
||||
except ChromecastConnectionError:
|
||||
break
|
||||
|
||||
# poll the socket
|
||||
can_read, _, _ = select.select([self.socket], [], [], POLL_TIME)
|
||||
|
||||
# read messages from chromecast
|
||||
message = data = None
|
||||
if self.socket in can_read and not self._force_recon:
|
||||
try:
|
||||
message = self._read_message()
|
||||
except InterruptLoop as exc:
|
||||
if self.stop.is_set():
|
||||
self.logger.info(
|
||||
"Stopped while reading message, disconnecting.")
|
||||
break
|
||||
else:
|
||||
self.logger.exception(
|
||||
"Interruption caught without being stopped %s",
|
||||
exc)
|
||||
break
|
||||
except ssl.SSLError as exc:
|
||||
if exc.errno == ssl.SSL_ERROR_EOF:
|
||||
if self.stop.is_set():
|
||||
break
|
||||
raise
|
||||
except socket.error:
|
||||
self._force_recon = True
|
||||
self.logger.info('Error reading from socket.')
|
||||
else:
|
||||
data = _json_from_message(message)
|
||||
if not message:
|
||||
continue
|
||||
|
||||
# If we are stopped after receiving a message we skip the message
|
||||
# and tear down the connection
|
||||
if self.stop.is_set():
|
||||
break
|
||||
|
||||
# See if any handlers will accept this message
|
||||
self._route_message(message, data)
|
||||
|
||||
if REQUEST_ID in data:
|
||||
callback = self._request_callbacks.pop(data[REQUEST_ID], None)
|
||||
if callback is not None:
|
||||
event = callback['event']
|
||||
callback['response'] = data
|
||||
|
||||
event.set()
|
||||
|
||||
# Clean up
|
||||
self._cleanup()
|
||||
|
||||
def _check_connection(self):
|
||||
"""
|
||||
Checks if the connection is active, and if not reconnect
|
||||
|
||||
:return: True if the connection is active, False if the connection was
|
||||
reset.
|
||||
"""
|
||||
# check if connection is expired
|
||||
reset = False
|
||||
if self._force_recon:
|
||||
self.logger.warning(
|
||||
"Error communicating with socket, resetting connection")
|
||||
reset = True
|
||||
|
||||
elif self.heartbeat_controller.is_expired():
|
||||
self.logger.warning("Heartbeat timeout, resetting connection")
|
||||
reset = True
|
||||
|
||||
if reset:
|
||||
self._report_connection_status(
|
||||
ConnectionStatus(CONNECTION_STATUS_LOST,
|
||||
NetworkAddress(self.host, self.port)))
|
||||
try:
|
||||
self.initialize_connection()
|
||||
except ChromecastConnectionError:
|
||||
self.stop.set()
|
||||
return False
|
||||
return True
|
||||
|
||||
def _route_message(self, message, data):
|
||||
""" Route message to any handlers on the message namespace """
|
||||
# route message to handlers
|
||||
if message.namespace in self._handlers:
|
||||
|
||||
# debug messages
|
||||
if message.namespace != NS_HEARTBEAT:
|
||||
self.logger.debug(
|
||||
"Received: %s", _message_to_string(message, data))
|
||||
|
||||
# message handlers
|
||||
try:
|
||||
handled = \
|
||||
self._handlers[message.namespace].receive_message(
|
||||
message, data)
|
||||
|
||||
if not handled:
|
||||
if data.get(REQUEST_ID) not in self._request_callbacks:
|
||||
self.logger.debug(
|
||||
"Message unhandled: %s",
|
||||
_message_to_string(message, data))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.logger.exception(
|
||||
(u"Exception caught while sending message to "
|
||||
u"controller %s: %s"),
|
||||
type(self._handlers[message.namespace]).__name__,
|
||||
_message_to_string(message, data))
|
||||
|
||||
else:
|
||||
self.logger.debug(
|
||||
"Received unknown namespace: %s",
|
||||
_message_to_string(message, data))
|
||||
|
||||
def _cleanup(self):
|
||||
""" Cleanup open channels and handlers """
|
||||
for channel in self._open_channels:
|
||||
try:
|
||||
self._disconnect_channel(channel)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
for handler in self._handlers.values():
|
||||
try:
|
||||
handler.tear_down()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
self.socket.close()
|
||||
self._report_connection_status(
|
||||
ConnectionStatus(CONNECTION_STATUS_DISCONNECTED,
|
||||
NetworkAddress(self.host, self.port)))
|
||||
self.connecting = True
|
||||
|
||||
def _report_connection_status(self, status):
|
||||
""" Report a change in the connection status to any listeners """
|
||||
for listener in self._connection_listeners:
|
||||
try:
|
||||
self.logger.debug("connection listener: %x (%s)",
|
||||
id(listener), type(listener).__name__)
|
||||
listener.new_connection_status(status)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
def _read_bytes_from_socket(self, msglen):
|
||||
""" Read bytes from the socket. """
|
||||
chunks = []
|
||||
bytes_recd = 0
|
||||
while bytes_recd < msglen:
|
||||
if self.stop.is_set():
|
||||
raise InterruptLoop("Stopped while reading from socket")
|
||||
try:
|
||||
chunk = self.socket.recv(min(msglen - bytes_recd, 2048))
|
||||
if chunk == b'':
|
||||
raise socket.error("socket connection broken")
|
||||
chunks.append(chunk)
|
||||
bytes_recd += len(chunk)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except ssl.SSLError as exc:
|
||||
# Support older ssl implementations which does not raise
|
||||
# socket.timeout on timeouts
|
||||
if _is_ssl_timeout(exc):
|
||||
continue
|
||||
raise
|
||||
return b''.join(chunks)
|
||||
|
||||
def _read_message(self):
|
||||
""" Reads a message from the socket and converts it to a message. """
|
||||
# first 4 bytes is Big-Endian payload length
|
||||
payload_info = self._read_bytes_from_socket(4)
|
||||
read_len = unpack(">I", payload_info)[0]
|
||||
|
||||
# now read the payload
|
||||
payload = self._read_bytes_from_socket(read_len)
|
||||
|
||||
# pylint: disable=no-member
|
||||
message = cast_channel_pb2.CastMessage()
|
||||
message.ParseFromString(payload)
|
||||
|
||||
return message
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def send_message(self, destination_id, namespace, data,
|
||||
inc_session_id=False, wait_for_response=False,
|
||||
no_add_request_id=False, force=False):
|
||||
""" Send a message to the Chromecast. """
|
||||
|
||||
# namespace is a string containing namespace
|
||||
# data is a dict that will be converted to json
|
||||
# wait_for_response only works if we have a request id
|
||||
|
||||
# If channel is not open yet, connect to it.
|
||||
self._ensure_channel_connected(destination_id)
|
||||
|
||||
request_id = None
|
||||
if not no_add_request_id:
|
||||
request_id = self._gen_request_id()
|
||||
data[REQUEST_ID] = request_id
|
||||
|
||||
if inc_session_id:
|
||||
data[SESSION_ID] = self.session_id
|
||||
|
||||
# pylint: disable=no-member
|
||||
msg = cast_channel_pb2.CastMessage()
|
||||
|
||||
msg.protocol_version = msg.CASTV2_1_0
|
||||
msg.source_id = self.source_id
|
||||
msg.destination_id = destination_id
|
||||
msg.payload_type = cast_channel_pb2.CastMessage.STRING
|
||||
msg.namespace = namespace
|
||||
msg.payload_utf8 = _json_to_payload(data)
|
||||
|
||||
# prepend message with Big-Endian 4 byte payload size
|
||||
be_size = pack(">I", msg.ByteSize())
|
||||
|
||||
# Log all messages except heartbeat
|
||||
if msg.namespace != NS_HEARTBEAT:
|
||||
self.logger.debug("Sending: %s", _message_to_string(msg, data))
|
||||
|
||||
if not force and self.stop.is_set():
|
||||
raise PyChromecastStopped("Socket client's thread is stopped.")
|
||||
if not self.connecting and not self._force_recon:
|
||||
try:
|
||||
self.socket.sendall(be_size + msg.SerializeToString())
|
||||
except socket.error:
|
||||
self._force_recon = True
|
||||
self.logger.info('Error writing to socket.')
|
||||
else:
|
||||
raise NotConnected("Chromecast is connecting...")
|
||||
|
||||
response = None
|
||||
if not no_add_request_id and wait_for_response:
|
||||
callback = self._request_callbacks[request_id] = {
|
||||
'event': threading.Event(),
|
||||
'response': None,
|
||||
}
|
||||
callback['event'].wait()
|
||||
response = callback.get('response')
|
||||
return response
|
||||
|
||||
def send_platform_message(self, namespace, message, inc_session_id=False,
|
||||
wait_for_response=False):
|
||||
""" Helper method to send a message to the platform. """
|
||||
return self.send_message(PLATFORM_DESTINATION_ID, namespace, message,
|
||||
inc_session_id, wait_for_response)
|
||||
|
||||
def send_app_message(self, namespace, message, inc_session_id=False,
|
||||
wait_for_response=False):
|
||||
""" Helper method to send a message to current running app. """
|
||||
if namespace not in self.app_namespaces:
|
||||
raise UnsupportedNamespace(
|
||||
("Namespace {} is not supported by current app. "
|
||||
"Supported are {}").format(namespace,
|
||||
", ".join(self.app_namespaces)))
|
||||
|
||||
return self.send_message(self.destination_id, namespace, message,
|
||||
inc_session_id, wait_for_response)
|
||||
|
||||
def register_connection_listener(self, listener):
|
||||
""" Register a connection listener for when the socket connection
|
||||
changes. Listeners will be called with
|
||||
listener.new_connection_status(status) """
|
||||
self._connection_listeners.append(listener)
|
||||
|
||||
def _ensure_channel_connected(self, destination_id):
|
||||
""" Ensure we opened a channel to destination_id. """
|
||||
if destination_id not in self._open_channels:
|
||||
self._open_channels.append(destination_id)
|
||||
|
||||
self.send_message(
|
||||
destination_id, NS_CONNECTION,
|
||||
{MESSAGE_TYPE: TYPE_CONNECT,
|
||||
'origin': {},
|
||||
'userAgent': 'PyChromecast',
|
||||
'senderInfo': {
|
||||
'sdkType': 2,
|
||||
'version': '15.605.1.3',
|
||||
'browserVersion': "44.0.2403.30",
|
||||
'platform': 4,
|
||||
'systemVersion': 'Macintosh; Intel Mac OS X10_10_3',
|
||||
'connectionType': 1}},
|
||||
no_add_request_id=True)
|
||||
|
||||
def _disconnect_channel(self, destination_id):
|
||||
""" Disconnect a channel with destination_id. """
|
||||
if destination_id in self._open_channels:
|
||||
self.send_message(
|
||||
destination_id, NS_CONNECTION,
|
||||
{MESSAGE_TYPE: TYPE_CLOSE, 'origin': {}},
|
||||
no_add_request_id=True, force=True)
|
||||
|
||||
self._open_channels.remove(destination_id)
|
||||
|
||||
self.handle_channel_disconnected()
|
||||
|
||||
def handle_channel_disconnected(self):
|
||||
""" Handles a channel being disconnected. """
|
||||
for namespace in self.app_namespaces:
|
||||
if namespace in self._handlers:
|
||||
self._handlers[namespace].channel_disconnected()
|
||||
|
||||
self.app_namespaces = []
|
||||
self.destination_id = None
|
||||
self.session_id = None
|
||||
|
||||
|
||||
class ConnectionController(BaseController):
|
||||
""" Controller to respond to connection messages. """
|
||||
|
||||
def __init__(self):
|
||||
super(ConnectionController, self).__init__(NS_CONNECTION)
|
||||
|
||||
def receive_message(self, message, data):
|
||||
""" Called when a connection message is received. """
|
||||
if self._socket_client.is_stopped:
|
||||
return True
|
||||
|
||||
if data[MESSAGE_TYPE] == TYPE_CLOSE:
|
||||
self._socket_client.handle_channel_disconnected()
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class HeartbeatController(BaseController):
|
||||
""" Controller to respond to heartbeat messages. """
|
||||
|
||||
def __init__(self):
|
||||
super(HeartbeatController, self).__init__(
|
||||
NS_HEARTBEAT, target_platform=True)
|
||||
self.last_ping = 0
|
||||
self.last_pong = time.time()
|
||||
|
||||
def receive_message(self, message, data):
|
||||
""" Called when a heartbeat message is received. """
|
||||
if self._socket_client.is_stopped:
|
||||
return True
|
||||
|
||||
if data[MESSAGE_TYPE] == TYPE_PING:
|
||||
try:
|
||||
self._socket_client.send_message(
|
||||
PLATFORM_DESTINATION_ID, self.namespace,
|
||||
{MESSAGE_TYPE: TYPE_PONG}, no_add_request_id=True)
|
||||
except PyChromecastStopped:
|
||||
self._socket_client.logger.exception(
|
||||
"Heartbeat error when sending response, "
|
||||
"Chromecast connection has stopped")
|
||||
|
||||
return True
|
||||
|
||||
elif data[MESSAGE_TYPE] == TYPE_PONG:
|
||||
self.reset()
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
def ping(self):
|
||||
""" Send a ping message. """
|
||||
self.last_ping = time.time()
|
||||
try:
|
||||
self.send_message({MESSAGE_TYPE: TYPE_PING})
|
||||
except NotConnected:
|
||||
self._socket_client.logger.error("Chromecast is disconnected. " +
|
||||
"Cannot ping until reconnected.")
|
||||
|
||||
def reset(self):
|
||||
""" Reset expired counter. """
|
||||
self.last_pong = time.time()
|
||||
|
||||
def is_expired(self):
|
||||
""" Indicates if connection has expired. """
|
||||
if time.time() - self.last_ping > HB_PING_TIME:
|
||||
self.ping()
|
||||
|
||||
return (time.time() - self.last_pong) > HB_PING_TIME + HB_PONG_TIME
|
||||
|
||||
|
||||
class ReceiverController(BaseController):
|
||||
"""
|
||||
Controller to interact with the Chromecast platform.
|
||||
|
||||
:param cast_type: Type of Chromecast device.
|
||||
"""
|
||||
|
||||
def __init__(self, cast_type=CAST_TYPE_CHROMECAST):
|
||||
super(ReceiverController, self).__init__(
|
||||
NS_RECEIVER, target_platform=True)
|
||||
|
||||
self.status = None
|
||||
self.launch_failure = None
|
||||
self.app_to_launch = None
|
||||
self.cast_type = cast_type
|
||||
self.app_launch_event = threading.Event()
|
||||
|
||||
self._status_listeners = []
|
||||
self._launch_error_listeners = []
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
""" Convenience method to retrieve current app id. """
|
||||
return self.status.app_id if self.status else None
|
||||
|
||||
def receive_message(self, message, data):
|
||||
""" Called when a receiver-message has been received. """
|
||||
if data[MESSAGE_TYPE] == TYPE_RECEIVER_STATUS:
|
||||
self._process_get_status(data)
|
||||
|
||||
return True
|
||||
|
||||
elif data[MESSAGE_TYPE] == TYPE_LAUNCH_ERROR:
|
||||
self._process_launch_error(data)
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
def register_status_listener(self, listener):
|
||||
""" Register a status listener for when a new Chromecast status
|
||||
has been received. Listeners will be called with
|
||||
listener.new_cast_status(status) """
|
||||
self._status_listeners.append(listener)
|
||||
|
||||
def register_launch_error_listener(self, listener):
|
||||
""" Register a listener for when a new launch error message
|
||||
has been received. Listeners will be called with
|
||||
listener.new_launch_error(launch_failure) """
|
||||
self._launch_error_listeners.append(listener)
|
||||
|
||||
def update_status(self, blocking=False):
|
||||
""" Sends a message to the Chromecast to update the status. """
|
||||
self.logger.debug("Receiver:Updating status")
|
||||
self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS},
|
||||
wait_for_response=blocking)
|
||||
|
||||
def launch_app(self, app_id, force_launch=False, block_till_launched=True):
|
||||
""" Launches an app on the Chromecast.
|
||||
|
||||
Will only launch if it is not currently running unless
|
||||
force_launch=True. """
|
||||
# If this is called too quickly after launch, we don't have the info.
|
||||
# We need the info if we are not force launching to check running app.
|
||||
if not force_launch and self.app_id is None:
|
||||
self.update_status(True)
|
||||
|
||||
if force_launch or self.app_id != app_id:
|
||||
self.logger.info("Receiver:Launching app %s", app_id)
|
||||
|
||||
# If we are blocking we need to wait for the status update or
|
||||
# launch failure before returning
|
||||
self.launch_failure = None
|
||||
if block_till_launched:
|
||||
self.app_to_launch = app_id
|
||||
self.app_launch_event.clear()
|
||||
|
||||
response = self.send_message({MESSAGE_TYPE: TYPE_LAUNCH,
|
||||
APP_ID: app_id},
|
||||
wait_for_response=block_till_launched)
|
||||
|
||||
if block_till_launched:
|
||||
is_app_started = False
|
||||
if response:
|
||||
response_type = response[MESSAGE_TYPE]
|
||||
if response_type == TYPE_RECEIVER_STATUS:
|
||||
new_status = self._parse_status(response,
|
||||
self.cast_type)
|
||||
new_app_id = new_status.app_id
|
||||
if new_app_id == app_id:
|
||||
is_app_started = True
|
||||
self.app_to_launch = None
|
||||
self.app_launch_event.clear()
|
||||
elif response_type == TYPE_LAUNCH_ERROR:
|
||||
self.app_to_launch = None
|
||||
self.app_launch_event.clear()
|
||||
launch_error = self._parse_launch_error(response)
|
||||
raise LaunchError(
|
||||
"Failed to launch app: {}, Reason: {}".format(
|
||||
app_id, launch_error.reason))
|
||||
|
||||
if not is_app_started:
|
||||
self.app_launch_event.wait()
|
||||
if self.launch_failure:
|
||||
raise LaunchError(
|
||||
"Failed to launch app: {}, Reason: {}".format(
|
||||
app_id, self.launch_failure.reason))
|
||||
else:
|
||||
self.logger.info(
|
||||
"Not launching app %s - already running", app_id)
|
||||
|
||||
def stop_app(self, block_till_stopped=True):
|
||||
""" Stops the current running app on the Chromecast. """
|
||||
self.logger.info("Receiver:Stopping current app '%s'", self.app_id)
|
||||
return self.send_message(
|
||||
{MESSAGE_TYPE: 'STOP'},
|
||||
inc_session_id=True, wait_for_response=block_till_stopped)
|
||||
|
||||
def set_volume(self, volume):
|
||||
""" Allows to set volume. Should be value between 0..1.
|
||||
Returns the new volume.
|
||||
|
||||
"""
|
||||
volume = min(max(0, volume), 1)
|
||||
self.logger.info("Receiver:setting volume to %.1f", volume)
|
||||
self.send_message({MESSAGE_TYPE: 'SET_VOLUME',
|
||||
'volume': {'level': volume}})
|
||||
return volume
|
||||
|
||||
def set_volume_muted(self, muted):
|
||||
""" Allows to mute volume. """
|
||||
self.send_message(
|
||||
{MESSAGE_TYPE: 'SET_VOLUME',
|
||||
'volume': {'muted': muted}})
|
||||
|
||||
@staticmethod
|
||||
def _parse_status(data, cast_type):
|
||||
"""
|
||||
Parses a STATUS message and returns a CastStatus object.
|
||||
|
||||
:type data: dict
|
||||
:param cast_type: Type of Chromecast.
|
||||
:rtype: CastStatus
|
||||
"""
|
||||
data = data.get('status', {})
|
||||
|
||||
volume_data = data.get('volume', {})
|
||||
|
||||
try:
|
||||
app_data = data['applications'][0]
|
||||
except KeyError:
|
||||
app_data = {}
|
||||
|
||||
is_audio = cast_type in (CAST_TYPE_AUDIO, CAST_TYPE_GROUP)
|
||||
|
||||
status = CastStatus(
|
||||
data.get('isActiveInput', None if is_audio else False),
|
||||
data.get('isStandBy', None if is_audio else True),
|
||||
volume_data.get('level', 1.0),
|
||||
volume_data.get('muted', False),
|
||||
app_data.get(APP_ID),
|
||||
app_data.get('displayName'),
|
||||
[item['name'] for item in app_data.get('namespaces', [])],
|
||||
app_data.get(SESSION_ID),
|
||||
app_data.get('transportId'),
|
||||
app_data.get('statusText', '')
|
||||
)
|
||||
return status
|
||||
|
||||
def _process_get_status(self, data):
|
||||
""" Processes a received STATUS message and notifies listeners. """
|
||||
status = self._parse_status(data, self.cast_type)
|
||||
is_new_app = self.app_id != status.app_id and self.app_to_launch
|
||||
self.status = status
|
||||
|
||||
if is_new_app and self.app_to_launch == self.app_id:
|
||||
self.app_to_launch = None
|
||||
self.app_launch_event.set()
|
||||
|
||||
self.logger.debug("Received status: %s", self.status)
|
||||
|
||||
self._report_status()
|
||||
|
||||
def _report_status(self):
|
||||
""" Reports the current status to all listeners. """
|
||||
for listener in self._status_listeners:
|
||||
try:
|
||||
listener.new_cast_status(self.status)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _parse_launch_error(data):
|
||||
"""
|
||||
Parses a LAUNCH_ERROR message and returns a LaunchFailure object.
|
||||
|
||||
:type data: dict
|
||||
:rtype: LaunchFailure
|
||||
"""
|
||||
return LaunchFailure(
|
||||
data.get(ERROR_REASON, None),
|
||||
data.get(APP_ID),
|
||||
data.get(REQUEST_ID),
|
||||
)
|
||||
|
||||
def _process_launch_error(self, data):
|
||||
"""
|
||||
Processes a received LAUNCH_ERROR message and notifies listeners.
|
||||
"""
|
||||
launch_failure = self._parse_launch_error(data)
|
||||
self.launch_failure = launch_failure
|
||||
|
||||
if self.app_to_launch:
|
||||
self.app_to_launch = None
|
||||
self.app_launch_event.set()
|
||||
|
||||
self.logger.debug("Launch status: %s", launch_failure)
|
||||
|
||||
for listener in self._launch_error_listeners:
|
||||
try:
|
||||
listener.new_launch_error(launch_failure)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
def tear_down(self):
|
||||
""" Called when controller is destroyed. """
|
||||
super(ReceiverController, self).tear_down()
|
||||
|
||||
self.status = None
|
||||
self.launch_failure = None
|
||||
self.app_to_launch = None
|
||||
self.app_launch_event.clear()
|
||||
self._report_status()
|
||||
|
||||
self._status_listeners[:] = []
|
||||
100
deps/pychromecast/upnp.py
vendored
Normal file
100
deps/pychromecast/upnp.py
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Module that implements UPNP protocol to discover Chromecasts
|
||||
"""
|
||||
import select
|
||||
import socket
|
||||
import logging
|
||||
import datetime as dt
|
||||
|
||||
# pylint: disable=import-error
|
||||
try: # Python 2
|
||||
import urlparse
|
||||
except ImportError: # Python 3
|
||||
import urllib.parse as urlparse
|
||||
|
||||
DISCOVER_TIMEOUT = 10
|
||||
|
||||
SSDP_ADDR = "239.255.255.250"
|
||||
SSDP_PORT = 1900
|
||||
SSDP_MX = 1
|
||||
SSDP_ST = "urn:dial-multiscreen-org:service:dial:1"
|
||||
|
||||
SSDP_REQUEST = 'M-SEARCH * HTTP/1.1\r\n' + \
|
||||
'HOST: {}:{:d}\r\n'.format(SSDP_ADDR, SSDP_PORT) + \
|
||||
'MAN: "ssdp:discover"\r\n' + \
|
||||
'MX: {:d}\r\n'.format(SSDP_MX) + \
|
||||
'ST: {}\r\n'.format(SSDP_ST) + \
|
||||
'\r\n'
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-branches
|
||||
def discover_chromecasts(max_devices=None, timeout=DISCOVER_TIMEOUT):
|
||||
"""
|
||||
Sends a message over the network to discover Chromecasts and returns
|
||||
a list of found IP addresses.
|
||||
|
||||
Inspired by Crimsdings
|
||||
https://github.com/crimsdings/ChromeCast/blob/master/cc_discovery.py
|
||||
"""
|
||||
ips = []
|
||||
|
||||
calc_now = dt.datetime.now
|
||||
start = calc_now()
|
||||
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
sock.sendto(SSDP_REQUEST.encode("ascii"), (SSDP_ADDR, SSDP_PORT))
|
||||
|
||||
sock.setblocking(0)
|
||||
|
||||
while True:
|
||||
time_diff = calc_now() - start
|
||||
|
||||
# pylint: disable=maybe-no-member
|
||||
seconds_left = timeout - time_diff.seconds
|
||||
|
||||
if seconds_left <= 0:
|
||||
return ips
|
||||
|
||||
ready = select.select([sock], [], [], seconds_left)[0]
|
||||
|
||||
if ready:
|
||||
response = sock.recv(1024).decode("ascii")
|
||||
|
||||
found_ip = found_st = None
|
||||
|
||||
headers = response.split("\r\n\r\n", 1)[0]
|
||||
|
||||
for header in headers.split("\r\n"):
|
||||
parts = header.split(": ", 1)
|
||||
|
||||
# Headers start with something like 'HTTP/1.1 200 OK'
|
||||
# We cannot split that up in key-value pair, so skip
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
|
||||
key, value = parts
|
||||
|
||||
if key == "LOCATION":
|
||||
url = urlparse.urlparse(value)
|
||||
|
||||
found_ip = url.hostname
|
||||
|
||||
elif key == "ST":
|
||||
found_st = value
|
||||
|
||||
if found_st == SSDP_ST and found_ip:
|
||||
ips.append(found_ip)
|
||||
|
||||
if max_devices and len(ips) == max_devices:
|
||||
return ips
|
||||
|
||||
except socket.error:
|
||||
logging.getLogger(__name__).exception(
|
||||
"Socket error while discovering Chromecasts")
|
||||
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
return ips
|
||||
425
deps/pychromecast/websocket.py
vendored
Normal file
425
deps/pychromecast/websocket.py
vendored
Normal file
@@ -0,0 +1,425 @@
|
||||
"""
|
||||
Implements the WEBSOCKET protocol to communicate with the Chromecast.
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
import threading
|
||||
import datetime as dt
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import weakref
|
||||
import atexit
|
||||
|
||||
import requests
|
||||
|
||||
from ws4py.client.threadedclient import WebSocketClient
|
||||
|
||||
from . import error
|
||||
|
||||
_DEBUG = False
|
||||
|
||||
RAMP_ENABLED = ['HBO_App',
|
||||
'18a8aeaa-8e3d-4c24-b05d-da68394a3476_1',
|
||||
'aa35235e-a960-4402-a87e-807ae8b2ac79',
|
||||
'06ee44ee-e7e3-4249-83b6-f5d0b6f07f34',
|
||||
'YouTube',
|
||||
'GoogleMusic',
|
||||
'06ee44ee-e7e3-4249-83b6-f5d0b6f07f34_1',
|
||||
'edaded98-5119-4c8a-afc1-de722da03562',
|
||||
'1812335e-441c-4e1e-a61a-312ca1ead90e',
|
||||
'Hulu_Plus',
|
||||
'Post_TV_App',
|
||||
'PlayMovies',
|
||||
'Songza_App',
|
||||
'Revision3_App']
|
||||
|
||||
PROTOCOL_RAMP = "ramp"
|
||||
PROTOCOL_COMMAND = "cm"
|
||||
|
||||
COMMAND_ATTR_TYPE = "type"
|
||||
COMMAND_TYPE_PING = "ping"
|
||||
COMMAND_TYPE_PONG = "pong"
|
||||
|
||||
RAMP_ATTR_TYPE = "type"
|
||||
RAMP_ATTR_CMD_ID = "cmd_id"
|
||||
RAMP_ATTR_TITLE = "title"
|
||||
RAMP_ATTR_SRC = "src"
|
||||
RAMP_ATTR_AUTOPLAY = "autoplay"
|
||||
RAMP_ATTR_STATUS = "status"
|
||||
RAMP_ATTR_CONTENT_INFO = "content_info"
|
||||
RAMP_ATTR_TIME_PROGRESS = "time_progress"
|
||||
RAMP_ATTR_VOLUME = "volume"
|
||||
RAMP_ATTR_POSITION = "position"
|
||||
|
||||
RAMP_STATUS_ATTR_ERROR = "error"
|
||||
RAMP_STATUS_ATTR_CONTENT_ID = "content_id"
|
||||
RAMP_STATUS_ATTR_TITLE = "title"
|
||||
RAMP_STATUS_ATTR_CURRENT_TIME = "current_time"
|
||||
RAMP_STATUS_ATTR_DURATION = "duration"
|
||||
RAMP_STATUS_ATTR_IMAGE_URL = "image_url"
|
||||
RAMP_STATUS_ATTR_VOLUME = "volume"
|
||||
RAMP_STATUS_ATTR_MUTED = "muted"
|
||||
RAMP_STATUS_ATTR_STATE = "state"
|
||||
RAMP_STATUS_CONTENT_INFO_ATTR_ALBUM_TITLE = "album_title"
|
||||
RAMP_STATUS_CONTENT_INFO_ATTR_ARTIST = "artist"
|
||||
RAMP_STATUS_ERROR_ATTR_CODE = "code"
|
||||
RAMP_STATUS_ERROR_ATTR_DOMAIN = "domain"
|
||||
RAMP_STATUS_ERROR_ATTR_EVENT_SEQUENCE = "event_sequence"
|
||||
|
||||
RAMP_TYPE_PLAY = "PLAY"
|
||||
RAMP_TYPE_STOP = "STOP"
|
||||
RAMP_TYPE_LOAD = "LOAD"
|
||||
RAMP_TYPE_STATUS = "STATUS"
|
||||
RAMP_TYPE_RESPONSE = "RESPONSE"
|
||||
RAMP_TYPE_VOLUME = "VOLUME"
|
||||
RAMP_TYPE_INFO = "INFO"
|
||||
|
||||
RAMP_STATE_UNKNOWN = 0
|
||||
RAMP_STATE_PLAYING = 2
|
||||
RAMP_STATE_STOPPED = 1
|
||||
|
||||
RAMP_VALUE_TRUE = "true"
|
||||
RAMP_VALUE_FALSE = "false"
|
||||
|
||||
_OPEN_CLIENTS = []
|
||||
|
||||
|
||||
def create_websocket_client(app_status):
|
||||
"""
|
||||
Creates and returns a RAMP client based on the supplied app status.
|
||||
Will return None if RAMP client is not supported.
|
||||
Will raise ValueError if unable to retrieve the websocket url.
|
||||
"""
|
||||
|
||||
# Check if current app has no service url or no protocols.
|
||||
if not app_status.service_url or not app_status.service_protocols:
|
||||
return None
|
||||
|
||||
req = requests.post(app_status.service_url,
|
||||
data="{}".encode("ascii"),
|
||||
headers={"Content-Type": "application/json"})
|
||||
|
||||
if req.status_code != 200:
|
||||
raise error.ChromecastConnectionError(
|
||||
"Could not retrieve websocket url ({}).".format(req.status_code))
|
||||
|
||||
conn_data = json.loads(req.text)
|
||||
|
||||
client = ChromecastWebSocketClient(conn_data['URL'],
|
||||
app_status.service_protocols)
|
||||
|
||||
client.connect()
|
||||
|
||||
atexit.register(_clean_open_clients)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def _clean_open_clients():
|
||||
""" Called on exit of Python to close open clients. """
|
||||
for client_weakref in list(_OPEN_CLIENTS):
|
||||
client = client_weakref()
|
||||
|
||||
if client and not client.terminated:
|
||||
client.close_connection()
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class ChromecastWebSocketClient(WebSocketClient):
|
||||
""" A Client to remote control a Chromecast via the RAMP protocol. """
|
||||
|
||||
def __init__(self, url, supported_protocols):
|
||||
WebSocketClient.__init__(self, url)
|
||||
|
||||
self.supported_protocols = supported_protocols
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.handlers = {}
|
||||
self._weakref = weakref.ref(self)
|
||||
|
||||
def opened(self):
|
||||
""" When connection is opened initiate the protocol handlers. """
|
||||
_OPEN_CLIENTS.append(self._weakref)
|
||||
|
||||
self.handlers[PROTOCOL_COMMAND] = CommandSubprotocol(self)
|
||||
|
||||
_known_prot = KNOWN_PROTOCOLS
|
||||
|
||||
# Instantiate supported subprotocols.
|
||||
for protocol in self.supported_protocols:
|
||||
handler = _known_prot.get(protocol)
|
||||
|
||||
if handler:
|
||||
self.handlers[protocol] = handler(self)
|
||||
else:
|
||||
self.logger.warning(
|
||||
"Unsupported protocol: {}".format(protocol))
|
||||
|
||||
def closed(self, code, reason=None):
|
||||
""" Clear protocol handlers when connection is lost. """
|
||||
# Clear reference to client
|
||||
_OPEN_CLIENTS.remove(self._weakref)
|
||||
|
||||
for handler in self.handlers.values():
|
||||
handler.client = None
|
||||
|
||||
self.handlers.clear()
|
||||
|
||||
def received_message(self, message):
|
||||
""" When a new message is received. """
|
||||
|
||||
# We do not support binary message
|
||||
if message.is_binary:
|
||||
return False
|
||||
|
||||
try:
|
||||
protocol, data = json.loads(message.data.decode('utf8'))
|
||||
except ValueError:
|
||||
# If error while parsing JSON
|
||||
# if unpack error: more then 2 items in the list
|
||||
logging.getLogger(__name__).exception(
|
||||
"Error parsing incoming message: {}".format(
|
||||
message.data.decode("utf8")))
|
||||
|
||||
return
|
||||
|
||||
if _DEBUG:
|
||||
logging.getLogger(__name__).info("Receiving {}".format(data))
|
||||
|
||||
handler = self.handlers.get(protocol)
|
||||
|
||||
if handler:
|
||||
handler._receive_protocol(data) # pylint: disable=protected-access
|
||||
else:
|
||||
logging.getLogger(__name__).warning(
|
||||
"Unknown protocol received: {}, {}".format(protocol, data))
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class BaseSubprotocol(object):
|
||||
""" Abstract implementation for a subprotocol. """
|
||||
|
||||
def __init__(self, protocol, client):
|
||||
self.protocol = protocol
|
||||
self.client = client
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def _send_protocol(self, data):
|
||||
""" Default handler for sending messages as subprotocol. """
|
||||
if _DEBUG:
|
||||
self.logger.info("Sending {}".format(data))
|
||||
|
||||
if not self.client:
|
||||
raise error.ChromecastConnectionError(
|
||||
"Not connected to Chromecast")
|
||||
|
||||
try:
|
||||
self.client.send(json.dumps([self.protocol, data]).encode("utf8"))
|
||||
|
||||
except socket.error:
|
||||
# if an error occured sending data over the socket
|
||||
raise error.ChromecastConnectionError(
|
||||
"Error communicating with Chromecast")
|
||||
|
||||
def _receive_protocol(self, data):
|
||||
""" Default handler for receiving messages as subprotocol. """
|
||||
self.logger.warning(
|
||||
"Unhandled {} message: {}".format(self.protocol, data))
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
""" Returns if this subprotocol is active. """
|
||||
return not self.client.terminated
|
||||
|
||||
|
||||
class CommandSubprotocol(BaseSubprotocol):
|
||||
""" Implements the Command subprotocol. """
|
||||
|
||||
def __init__(self, client):
|
||||
BaseSubprotocol.__init__(self, PROTOCOL_COMMAND, client)
|
||||
|
||||
def _receive_protocol(self, data):
|
||||
""" Handles an incoming COMMAND message. """
|
||||
|
||||
if data[COMMAND_ATTR_TYPE] == COMMAND_TYPE_PING:
|
||||
self._send_protocol({COMMAND_ATTR_TYPE: COMMAND_TYPE_PONG})
|
||||
else:
|
||||
BaseSubprotocol._receive_protocol(self, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, attribute-defined-outside-init
|
||||
class RampSubprotocol(BaseSubprotocol):
|
||||
""" Implements the Ramp subprotocol. """
|
||||
|
||||
def __init__(self, client):
|
||||
BaseSubprotocol.__init__(self, PROTOCOL_RAMP, client)
|
||||
|
||||
self.command_id = 0
|
||||
self.commands = {}
|
||||
|
||||
self._update_status({})
|
||||
|
||||
def _receive_protocol(self, data):
|
||||
""" Handles an incoming Ramp message. """
|
||||
message_type = data[RAMP_ATTR_TYPE]
|
||||
|
||||
if message_type == RAMP_TYPE_STATUS:
|
||||
self._update_status(data[RAMP_ATTR_STATUS])
|
||||
|
||||
elif message_type == RAMP_TYPE_RESPONSE:
|
||||
# Match it with the command that we send
|
||||
try:
|
||||
cmd_type, cmd_event = \
|
||||
self.commands.pop(data[RAMP_ATTR_CMD_ID])
|
||||
|
||||
except KeyError:
|
||||
# If CMD_ID did not exist or we do not recognize command
|
||||
return
|
||||
|
||||
# Handle response, currently no response handlers
|
||||
if cmd_type in (RAMP_TYPE_PLAY, RAMP_TYPE_VOLUME,
|
||||
RAMP_TYPE_INFO):
|
||||
|
||||
self._update_status(data[RAMP_ATTR_STATUS])
|
||||
|
||||
else:
|
||||
self.logger.warning(
|
||||
"Unhandled response for command {}: {}".format(
|
||||
cmd_type, data))
|
||||
|
||||
# Alert code that is waiting for this command to get response
|
||||
if cmd_event:
|
||||
cmd_event.set()
|
||||
|
||||
else:
|
||||
BaseSubprotocol._receive_protocol(self, data)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def _send_ramp(self, data, blocking=False):
|
||||
"""
|
||||
Sends a RAMP message.
|
||||
Set blocking=True to wait till the Chromecast sends a response
|
||||
to the command.
|
||||
"""
|
||||
data[RAMP_ATTR_CMD_ID] = self.command_id
|
||||
|
||||
event = threading.Event() if blocking else None
|
||||
|
||||
# Save type to match later with response
|
||||
self.commands[self.command_id] = (data[RAMP_ATTR_TYPE], event)
|
||||
|
||||
self._send_protocol(data)
|
||||
|
||||
self.command_id += 1
|
||||
|
||||
if blocking:
|
||||
event.wait()
|
||||
|
||||
@property
|
||||
def is_playing(self):
|
||||
""" Property that represents if content is being played. """
|
||||
return self.is_active and self.state == RAMP_STATE_PLAYING
|
||||
|
||||
def play(self):
|
||||
""" Send the PLAY-command to the RAMP-target. """
|
||||
self._send_ramp({RAMP_ATTR_TYPE: RAMP_TYPE_PLAY})
|
||||
|
||||
def pause(self):
|
||||
""" Send the PAUSE-command to the RAMP-target. """
|
||||
# The STOP command actually pauses the media
|
||||
self._send_ramp({RAMP_ATTR_TYPE: RAMP_TYPE_STOP})
|
||||
|
||||
def playpause(self):
|
||||
""" Plays if paused, pauses if playing. """
|
||||
if self.state == RAMP_STATE_PLAYING:
|
||||
self.pause()
|
||||
else:
|
||||
self.play()
|
||||
|
||||
def seek(self, seconds):
|
||||
""" Seek within the content played at RAMP-target. """
|
||||
self._send_ramp({RAMP_ATTR_TYPE: RAMP_TYPE_PLAY,
|
||||
RAMP_ATTR_POSITION: seconds})
|
||||
|
||||
def rewind(self):
|
||||
""" Rewinds current media item. """
|
||||
self.seek(0)
|
||||
|
||||
def next(self):
|
||||
""" Skip to the next content at the RAMP-target. """
|
||||
if self.duration != 0:
|
||||
self.seek(self.duration-.1)
|
||||
|
||||
def set_volume(self, volume):
|
||||
""" Set volume at the RAMP-target. """
|
||||
# volume is double between 0 and 1
|
||||
self._send_ramp({RAMP_ATTR_TYPE: RAMP_TYPE_VOLUME,
|
||||
RAMP_ATTR_VOLUME: volume})
|
||||
|
||||
def volume_up(self):
|
||||
""" Increases volume. """
|
||||
if self.volume < 1:
|
||||
self.set_volume(min(self.volume+.1, 1))
|
||||
|
||||
def volume_down(self):
|
||||
""" Decreases volume. """
|
||||
if self.volume > 0:
|
||||
self.set_volume(max(self.volume-.1, 0))
|
||||
|
||||
def refresh(self):
|
||||
""" Refresh data at the RAMP-target. """
|
||||
self._send_ramp({RAMP_ATTR_TYPE: RAMP_TYPE_INFO})
|
||||
|
||||
def _update_status(self, status):
|
||||
""" Updates the RAMP status. """
|
||||
con_inf = status.get(RAMP_ATTR_CONTENT_INFO, {})
|
||||
|
||||
self.state = status.get(RAMP_STATUS_ATTR_STATE, 0)
|
||||
self.volume = status.get(RAMP_STATUS_ATTR_VOLUME, 1)
|
||||
self.muted = status.get(RAMP_STATUS_ATTR_MUTED, False)
|
||||
self.content_id = status.get(RAMP_STATUS_ATTR_CONTENT_ID)
|
||||
self.title = status.get(RAMP_STATUS_ATTR_TITLE)
|
||||
self.artist = con_inf.get(RAMP_STATUS_CONTENT_INFO_ATTR_ARTIST)
|
||||
self.album = con_inf.get(RAMP_STATUS_CONTENT_INFO_ATTR_ALBUM_TITLE)
|
||||
self._current_time = status.get(RAMP_STATUS_ATTR_CURRENT_TIME, 0)
|
||||
self.duration = status.get(RAMP_STATUS_ATTR_DURATION, 0)
|
||||
self.image_url = status.get(RAMP_STATUS_ATTR_IMAGE_URL)
|
||||
self.time_progress = status.get(RAMP_ATTR_TIME_PROGRESS, False)
|
||||
|
||||
self.last_updated = dt.datetime.now()
|
||||
|
||||
@property
|
||||
def current_time(self):
|
||||
""" Returns current time of the content. """
|
||||
|
||||
# If time is progressing we have to calculate the current time based on
|
||||
# time the status was retrieved and the then current time.
|
||||
if self.time_progress:
|
||||
timediff = dt.datetime.now() - self.last_updated
|
||||
|
||||
# pylint: disable=maybe-no-member
|
||||
return min(self._current_time + timediff.seconds, self.duration)
|
||||
|
||||
else:
|
||||
return self._current_time
|
||||
|
||||
def __repr__(self):
|
||||
if self.state == RAMP_STATE_PLAYING:
|
||||
state = "playing"
|
||||
elif self.state == RAMP_STATE_STOPPED:
|
||||
state = "stopped"
|
||||
else:
|
||||
state = "unknown state"
|
||||
|
||||
if self.title:
|
||||
return "RampSubprotocol({}, {}/{}, {})".format(
|
||||
self.title.encode("ascii", "replace"),
|
||||
self.current_time, self.duration, state)
|
||||
else:
|
||||
return "RampSubprotocol({})".format(state)
|
||||
|
||||
|
||||
KNOWN_PROTOCOLS = {
|
||||
PROTOCOL_COMMAND: CommandSubprotocol,
|
||||
PROTOCOL_RAMP: RampSubprotocol,
|
||||
}
|
||||
41
deps/pychromecast/youtube.py
vendored
Normal file
41
deps/pychromecast/youtube.py
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Methods to interface with the YouTube application.
|
||||
"""
|
||||
from .config import APP_ID
|
||||
from .error import NoChromecastFoundError
|
||||
|
||||
|
||||
def play_youtube_video(video_id, host=None, cast=None, **filters):
|
||||
""" Starts the YouTube app if it is not running and plays
|
||||
specified video. """
|
||||
|
||||
data = {"v": video_id}
|
||||
|
||||
_start_youtube(data, host, cast, **filters)
|
||||
|
||||
|
||||
def play_youtube_playlist(playlist_id, host=None, cast=None, **filters):
|
||||
""" Starts the YouTube app if it is not running and plays
|
||||
specified playlist. """
|
||||
|
||||
data = {"listType": "playlist", "list": playlist_id}
|
||||
|
||||
_start_youtube(data, host, cast, **filters)
|
||||
|
||||
|
||||
def _start_youtube(data, host, cast, **filters):
|
||||
""" Helper method to launch the YouTube application. """
|
||||
|
||||
# For backwards compatibility with 0.4.4
|
||||
if host:
|
||||
filters['ip'] = host
|
||||
|
||||
if not cast:
|
||||
from . import get_chromecast
|
||||
|
||||
cast = get_chromecast(strict=False, **filters)
|
||||
|
||||
if not cast:
|
||||
raise NoChromecastFoundError()
|
||||
|
||||
cast.start_app(APP_ID["YOUTUBE"], data)
|
||||
Reference in New Issue
Block a user