[Python-modules-commits] [pychromecast] 01/03: Import pychromecast_0.7.3.orig.tar.gz

Ruben Undheim rubund-guest at moszumanska.debian.org
Fri May 6 18:17:21 UTC 2016


This is an automated email from the git hooks/post-receive script.

rubund-guest pushed a commit to branch master
in repository pychromecast.

commit 684442b9f7873e0b3b6d57d901ce868eae26c184
Author: Ruben Undheim <ruben.undheim at gmail.com>
Date:   Fri May 6 19:33:25 2016 +0200

    Import pychromecast_0.7.3.orig.tar.gz
---
 .gitignore                           |   3 +
 MANIFEST.in                          |   5 +
 README.md => README.rst              | 104 +++++---
 __init__.py                          |   0
 fabfile.py                           |  25 ++
 pychromecast/__init__.py             | 220 ++++++++++++++--
 pychromecast/controllers/__init__.py |   3 +-
 pychromecast/controllers/media.py    |  36 ++-
 pychromecast/dial.py                 |  53 +++-
 pychromecast/discovery.py            |  35 ++-
 pychromecast/error.py                |   5 +
 pychromecast/socket_client.py        | 491 +++++++++++++++++++++++++++++------
 requirements.txt                     |   5 +-
 setup.py                             |   7 +-
 14 files changed, 833 insertions(+), 159 deletions(-)

diff --git a/.gitignore b/.gitignore
index b2e6791..a49dcb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,9 @@ develop-eggs
 lib
 lib64
 
+# Build files
+README.rst
+
 # Installer logs
 pip-log.txt
 
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..a49e586
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+include README.rst
+include LICENSE
+include requirements.txt
+graft pychromecast
+recursive-exclude * *.py[co]
diff --git a/README.md b/README.rst
similarity index 55%
rename from README.md
rename to README.rst
index 66f6221..6c528fe 100644
--- a/README.md
+++ b/README.rst
@@ -1,25 +1,41 @@
-pychromecast [![Build Status](https://travis-ci.org/balloob/pychromecast.svg?branch=master)](https://travis-ci.org/balloob/pychromecast)
-============
+pychromecast |Build Status|
+===========================
 
-Library for Python 2 and 3 to communicate with the Google Chromecast. It currently supports:
- - Auto discovering connected Chromecasts on the network
- - Start the default media receiver and play any online media
- - Control playback of current playing media
- - Implement Google Chromecast api v2
- - Communicate with apps via channels
- - Easily extendable to add support for unsupported namespaces
+.. |Build Status| image:: https://travis-ci.org/balloob/pychromecast.svg?branch=master
+   :target: https://travis-ci.org/balloob/pychromecast
 
-*PyChromecast 0.6 introduces some backward incompatible changes due to the migration from DIAL to socket for retrieving the app status.*
+Library for Python 2 and 3 to communicate with the Google Chromecast. It
+currently supports:
+
+-  Auto discovering connected Chromecasts on the network
+-  Start the default media receiver and play any online media
+-  Control playback of current playing media
+-  Implement Google Chromecast api v2
+-  Communicate with apps via channels
+-  Easily extendable to add support for unsupported namespaces
+-  Multi-room setups with Audio cast devices
+
+*PyChromecast 0.6 introduces some backward incompatible changes due to
+the migration from DIAL to socket for retrieving the app status.*
 
 Dependencies
 ------------
-PyChromecast depends on the Python packages requests, protobuf and zeroconf. Make sure you have these dependencies installed using `pip install -r requirements.txt`
 
-_Some users running Python 2.7 have [reported](https://github.com/balloob/pychromecast/issues/47#issuecomment-107822162) that they had to upgrade their version of pip using `pip install --upgrade pip` before they were able to install the latest version of the dependencies._
+PyChromecast depends on the Python packages requests, protobuf and
+zeroconf. Make sure you have these dependencies installed using
+``pip install -r requirements.txt``
+
+Some users running Python 2.7 have `reported`_ that they had to upgrade
+their version of pip using ``pip install --upgrade pip`` before they
+were able to install the latest version of the dependencies.
+
+.. _reported: https://github.com/balloob/pychromecast/issues/47#issuecomment-107822162
 
 How to use
 ----------
 
+.. code:: python
+
     >> from __future__ import print_function
     >> import time
     >> import pychromecast
@@ -28,54 +44,61 @@ How to use
     ['Dev', 'Living Room', 'Den', 'Bedroom']
 
     >> cast = pychromecast.get_chromecast(friendly_name="Living Room")
+    >> # Wait for cast device to be ready
+    >> cast.wait()
     >> print(cast.device)
-    DeviceStatus(friendly_name='Living Room', model_name='Eureka Dongle', manufacturer='Google Inc.', api_version=(1, 0))
+    DeviceStatus(friendly_name='Living Room', model_name='Chromecast', manufacturer='Google Inc.', api_version=(1, 0), uuid=UUID('df6944da-f016-4cb8-97d0-3da2ccaa380b'), cast_type='cast')
 
     >> print(cast.status)
     CastStatus(is_active_input=True, is_stand_by=False, volume_level=1.0, volume_muted=False, app_id=u'CC1AD845', display_name=u'Default Media Receiver', namespaces=[u'urn:x-cast:com.google.cast.player.message', u'urn:x-cast:com.google.cast.media'], session_id=u'CCA39713-9A4F-34A6-A8BF-5D97BE7ECA5C', transport_id=u'web-9', status_text='')
 
     >> mc = cast.media_controller
+    >> mc.play_media('http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', 'video/mp4')
     >> print(mc.status)
     MediaStatus(current_time=42.458322, content_id=u'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', content_type=u'video/mp4', duration=596.474195, stream_type=u'BUFFERED', idle_reason=None, media_session_id=1, playback_rate=1, player_state=u'PLAYING', supported_media_commands=15, volume_level=1, volume_muted=False)
 
     >> mc.pause()
     >> time.sleep(5)
     >> mc.play()
-    >> from pychromecast.controllers.youtube import YouTubeController
-    >> yt = YouTubeController()
-    >> cast.register_handler(yt)
-    >> yt.play_video("L0MK7qz13bU")
 
 Adding support for extra namespaces
 -----------------------------------
-Each app that runs on the Chromecast supports namespaces. They specify a JSON-based mini-protocol. This is used to communicate between the Chromecast and your phone/browser and now Python.
+
+Each app that runs on the Chromecast supports namespaces. They specify a
+JSON-based mini-protocol. This is used to communicate between the
+Chromecast and your phone/browser and now Python.
 
 Support for extra namespaces is added by using controllers. To add your own namespace to a current chromecast instance you will first have to define your controller. Example of a minimal controller:
 
+.. code:: python
+
     from pychromecast.controllers import BaseController
 
     class MyController(BaseController):
         def __init__(self):
-            super(MediaController, self).__init__(
+            super(MyController, self).__init__(
                 "urn:x-cast:my.super.awesome.namespace")
 
         def receive_message(self, message, data):
             print("Wow, I received this message: {}".format(data))
 
-            return True # indicate you handled this message
+            return True  # indicate you handled this message
 
         def request_beer(self):
             self.send_message({'request': 'beer'})
 
 After you have defined your controller you will have to add an instance to a Chromecast object: `cast.register_handler(MyController())`. When a message is received with your namespace it will be routed to your controller.
 
-For more options see the [BaseController](https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/__init__.py). For an example of a fully implemented controller see the [MediaController](https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/media.py).
+For more options see the `BaseController`_. For an example of a fully implemented controller see the `MediaController`_.
+
+.. _BaseController: https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/__init__.py
+.. _MediaController: https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/media.py
 
 Exploring existing namespaces
 -------------------------------
 So you've got PyChromecast running and decided it is time to add support to your favorite app. No worries, the following instructions will have you covered in exploring the possibilities.
 
-The following instructions require the use of the [Google Chrome browser](https://www.google.com/chrome/) and the [Google Cast plugin](https://chrome.google.com/webstore/detail/google-cast/boadgeojelhgndaghljhdicfkmllpafd).
+The following instructions require the use of the `Google Chrome browser`_ and the `Google Cast plugin`_.
 
  * In Chrome, go to `chrome://net-internals/#capture`
  * Enable the checkbox 'Include the actual bytes sent/received.'
@@ -86,29 +109,50 @@ The following instructions require the use of the [Google Chrome browser](https:
  * Go through the results and collect the JSON that is exchanged.
  * Now write a controller that is able to mimic this behavior :-)
 
+.. _Google Chrome Browser: https://www.google.com/chrome/
+.. _Google Cast Plugin: https://chrome.google.com/webstore/detail/google-cast/boadgeojelhgndaghljhdicfkmllpafd
+
 Ignoring CEC Data
 -----------------
 The Chromecast typically reports whether it is the active input on the device
 to which it is connected. This value is stored inside a cast object in the
 following property.
 
+.. code:: python
+
     cast.status.is_active_input
 
 Some Chromecast users have reported CEC incompatibilities with their media
 center devices. These incompatibilities may sometimes cause this active input
 value to be reported improperly.
 
-This active input value is typically used to determine if the Chromecast is
-idle. PyChromecast is capable of ignoring the active input value when
-determining if the Chromecast is idle in the instance that the Chromecast is
-returning erroneous values. To ignore this CEC detection data in PyChromecast,
-append a [Linux style wildcard](http://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm)
-formatted string to the IGNORE_CEC list in PyChromecast like in the example
-below.
+This active input value is typically used to determine if the Chromecast
+is idle. PyChromecast is capable of ignoring the active input value when
+determining if the Chromecast is idle in the instance that the
+Chromecast is returning erroneous values. To ignore this CEC detection
+data in PyChromecast, append a `Linux style wildcard`_ formatted string
+to the IGNORE\_CEC list in PyChromecast like in the example below.
+
+.. code:: python
 
     pychromecast.IGNORE_CEC.append('*')  # Ignore CEC on all devices
     pychromecast.IGNORE_CEC.append('Living Room')  # Ignore CEC on Chromecasts named Living Room
 
+Maintainers
+-----------
+
+-  Jan Borsodi (`@am0s`_)
+-  Ryan Kraus (`@rmkraus`_)
+-  Paulus Schoutsen (`@balloob`_, original author)
+
 Thanks
 ------
-I would like to thank [Fred Clift](https://github.com/minektur) for laying the socket client ground work. Without him it would not have been possible!
+
+I would like to thank `Fred Clift`_ for laying the socket client ground
+work. Without him it would not have been possible!
+
+.. _Linux style wildcard: http://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm
+.. _ at am0s: https://github.com/am0s
+.. _ at rmkraus: https://github.com/rmkraus
+.. _ at balloob: https://github.com/balloob
+.. _Fred Clift: https://github.com/minektur
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fabfile.py b/fabfile.py
new file mode 100644
index 0000000..8525cb6
--- /dev/null
+++ b/fabfile.py
@@ -0,0 +1,25 @@
+import os
+from fabric.decorators import task
+from fabric.operations import local
+
+
+ at task
+def build():
+    """
+    Builds the distribution files
+    """
+    if not os.path.exists("build"):
+        os.mkdir("build")
+    local("date >> build/log")
+    local("python setup.py sdist >> build/log")
+    local("python setup.py bdist_wheel >> build/log")
+
+
+ at task
+def release():
+    """
+    Uploads files to PyPi to create a new release.
+
+    Note: Requires that files have been built first
+    """
+    local("twine upload dist/*")
diff --git a/pychromecast/__init__.py b/pychromecast/__init__.py
index b141a66..f37fec8 100644
--- a/pychromecast/__init__.py
+++ b/pychromecast/__init__.py
@@ -3,37 +3,63 @@ 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
+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):
+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, _ in hosts:
+    for ip_address, port, uuid, model_name, friendly_name in hosts:
         try:
-            cc_list.append(Chromecast(host=ip_address, tries=tries))
+            # 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:
             pass
     return cc_list
 
 
-def get_chromecasts(tries=None, **filters):
+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.
@@ -53,11 +79,13 @@ def get_chromecasts(tries=None, **filters):
     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.
+    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=tries))
+    cc_list = set(_get_all_chromecasts(tries, retry_wait, timeout))
     excluded_cc = set()
 
     if not filters:
@@ -84,7 +112,8 @@ def get_chromecasts(tries=None, **filters):
     return list(filtered_cc)
 
 
-def get_chromecasts_as_dict(tries=None, **filters):
+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.
@@ -92,13 +121,18 @@ def get_chromecasts_as_dict(tries=None, **filters):
     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.
+    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, **filters)}
+            for cc in get_chromecasts(tries=tries, retry_wait=retry_wait,
+                                      timeout=timeout,
+                                      **filters)}
 
 
-def get_chromecast(strict=False, tries=None, **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.
@@ -110,16 +144,22 @@ def get_chromecast(strict=False, tries=None, **filters):
     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.
+    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, **filters)
+        results = get_chromecasts(tries=tries, retry_wait=retry_wait,
+                                  timeout=timeout,
+                                  **filters)
     else:
-        results = _get_all_chromecasts(tries)
+        results = _get_all_chromecasts(tries, retry_wait)
 
     if len(results) > 1:
         if strict:
@@ -143,24 +183,72 @@ def get_chromecast(strict=False, tries=None, **filters):
 
 # pylint: disable=too-many-instance-attributes
 class Chromecast(object):
-    """ Class to interface with a ChromeCast. """
+    """
+    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)
 
-    def __init__(self, host, tries=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 = get_device_status(self.host)
+        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(
-                "Could not connect to {}".format(self.host))
+                "Could not connect to {}:{}".format(self.host, self.port))
 
         self.status = None
+        self.status_event = threading.Event()
 
-        self.socket_client = socket_client.SocketClient(host, tries)
+        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)
@@ -170,6 +258,12 @@ class Chromecast(object):
         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()
 
@@ -188,6 +282,37 @@ class Chromecast(object):
                 (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
@@ -205,6 +330,8 @@ class Chromecast(object):
     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. """
@@ -237,11 +364,58 @@ class Chromecast(object):
         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):
         self.socket_client.stop.set()
 
     def __repr__(self):
-        return "Chromecast({}, {}, {}, {}, api={}.{})".format(
-            self.host, self.device.friendly_name, self.device.model_name,
-            self.device.manufacturer, self.device.api_version[0],
-            self.device.api_version[1])
+        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])
diff --git a/pychromecast/controllers/__init__.py b/pychromecast/controllers/__init__.py
index 4aa99ab..c2fe4b5 100644
--- a/pychromecast/controllers/__init__.py
+++ b/pychromecast/controllers/__init__.py
@@ -80,7 +80,7 @@ class BaseController(object):
                     ("Namespace {} is not supported by running"
                      "application.").format(self.namespace))
 
-        self._message_func(
+        return self._message_func(
             self.namespace, data, inc_session_id, wait_for_response)
 
     # pylint: disable=unused-argument,no-self-use
@@ -94,6 +94,7 @@ class BaseController(object):
     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. """
diff --git a/pychromecast/controllers/media.py b/pychromecast/controllers/media.py
index a305990..8260ac6 100644
--- a/pychromecast/controllers/media.py
+++ b/pychromecast/controllers/media.py
@@ -394,9 +394,29 @@ class MediaController(BaseController):
     # 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):
-        """ Plays media on the Chromecast. Start default media receiver if not
-            already started. """
+                   stream_type=STREAM_TYPE_BUFFERED,
+                   metadata=None):
+        """
+        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".
+        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(APP_MEDIA_RECEIVER)
 
@@ -405,6 +425,7 @@ class MediaController(BaseController):
                 'contentId': url,
                 'streamType': stream_type,
                 'contentType': content_type,
+                'metadata': metadata or {}
             },
             MESSAGE_TYPE: TYPE_LOAD,
             'currentTime': current_time,
@@ -413,10 +434,15 @@ class MediaController(BaseController):
         }
 
         if title:
-            msg['customData']['payload']['title'] = title
+            msg['media']['metadata']['title'] = title
 
         if thumb:
-            msg['customData']['payload']['thumb'] = thumb
+            msg['media']['metadata']['thumb'] = thumb
+
+            if 'images' not in msg['media']['metadata']:
+                msg['media']['metadata']['images'] = []
+
+            msg['media']['metadata']['images'].append({'url': thumb})
 
         self.send_message(msg, inc_session_id=True)
 
diff --git a/pychromecast/dial.py b/pychromecast/dial.py
index 40e17b5..57b7c4a 100644
--- a/pychromecast/dial.py
+++ b/pychromecast/dial.py
@@ -3,9 +3,10 @@ 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}"
 
@@ -14,6 +15,19 @@ 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. """
@@ -22,13 +36,27 @@ def reboot(host):
 
 
 def get_device_status(host):
-    """ Returns the device status as a named tuple. """
+    """
+    :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")
@@ -41,14 +69,24 @@ def get_device_status(host):
         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)
+                            api_version, uuid, cast_type)
 
     except (requests.exceptions.RequestException, ET.ParseError):
         return None
@@ -57,7 +95,11 @@ def get_device_status(host):
 def _read_xml_element(element, xml_ns, tag_name, default=""):
     """ Helper method to read text from an element. """
     try:
-        return element.find(xml_ns + tag_name).text
+        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
@@ -65,4 +107,5 @@ def _read_xml_element(element, xml_ns, tag_name, default=""):
 
 DeviceStatus = namedtuple("DeviceStatus",
                           ["friendly_name", "model_name",
-                           "manufacturer", "api_version"])
+                           "manufacturer", "api_version",
+                           "uuid", "cast_type"])
diff --git a/pychromecast/discovery.py b/pychromecast/discovery.py
index 50bbd17..9b7efb5 100644
--- a/pychromecast/discovery.py
+++ b/pychromecast/discovery.py
@@ -1,6 +1,8 @@
 """Discovers Chromecasts on the network using mDNS/zeroconf."""
 import time
+from uuid import UUID
 
+import six
 from zeroconf import ServiceBrowser, Zeroconf
 
 DISCOVER_TIMEOUT = 5
@@ -31,14 +33,37 @@ class CastListener(object):
         service = None
         tries = 0
         while service is None and tries < 4:
-            service = zconf.get_service_info(typ, name)
+            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 service:
-            ips = zconf.cache.entries_with_name(service.server.lower())
-            host = repr(ips[0]) if ips else service.server
+        if not service:
+            return
 
-            self.services[name] = (host, service.port)
+        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):
diff --git a/pychromecast/error.py b/pychromecast/error.py
index e121a6a..f214af7 100644
--- a/pychromecast/error.py
+++ b/pychromecast/error.py
@@ -27,6 +27,11 @@ class ChromecastConnectionError(PyChromecastError):
     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.
diff --git a/pychromecast/socket_client.py b/pychromecast/socket_client.py
index fa64140..ee76f9e 100644
--- a/pychromecast/socket_client.py
+++ b/pychromecast/socket_client.py
@@ -17,16 +17,18 @@ import threading
 import time
 from collections import namedtuple
 from struct import pack, unpack
+import sys
 
 from . import cast_channel_pb2
-import sys
+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
+    PyChromecastStopped,
+    LaunchError,
 )
 
 NS_CONNECTION = 'urn:x-cast:com.google.cast.tp.connection'
@@ -43,16 +45,35 @@ 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):
@@ -79,6 +100,21 @@ else:
         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',
@@ -86,11 +122,33 @@ CastStatus = namedtuple('CastStatus',
                          '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. """
+    """
+    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)
... 698 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/pychromecast.git



More information about the Python-modules-commits mailing list