Initial Configuration Push

This commit is contained in:
CCOSTAN
2016-10-11 16:42:06 +00:00
parent b83eeadfcb
commit 5127bc2109
2145 changed files with 298464 additions and 0 deletions

BIN
deps/pychromecast/.DS_Store vendored Normal file

Binary file not shown.

424
deps/pychromecast/__init__.py vendored Normal file
View 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])

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

118
deps/pychromecast/authority_keys_pb2.py vendored Normal file
View 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
View 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
View 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 {}

View 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."))

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

468
deps/pychromecast/controllers/media.py vendored Normal file
View 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
View 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})

View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

945
deps/pychromecast/socket_client.py vendored Normal file
View 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
View 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
View 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
View 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)