Hello community,
here is the log from the commit of package python-PyChromecast for
openSUSE:Factory checked in at 2019-06-18 15:00:21
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-PyChromecast (Old)
and /work/SRC/openSUSE:Factory/.python-PyChromecast.new.4811 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-PyChromecast"
Tue Jun 18 15:00:21 2019 rev:5 rq:710551 version:3.2.2
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-PyChromecast/python-PyChromecast.changes
2019-03-06 15:50:09.128433168 +0100
+++
/work/SRC/openSUSE:Factory/.python-PyChromecast.new.4811/python-PyChromecast.changes
2019-06-18 15:00:24.433268016 +0200
@@ -1,0 +2,12 @@
+Tue Jun 18 11:28:34 UTC 2019 - Marketa Calabkova <[email protected]>
+
+- Update to 3.2.2
+ * Improve matching of spotify device to handle audio groups
+ * Fix broken attempt to update status during tear down
+ * Add google home mini as audio device
+ * Add support for queue_next / queue_prev
+ * Take expiration from login and pass to controller
+ * Add multizone controller and multizone manager
+ * Remove the filters feature from get_chromecasts
+
+-------------------------------------------------------------------
Old:
----
PyChromecast-2.5.2.tar.gz
New:
----
PyChromecast-3.2.2.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-PyChromecast.spec ++++++
--- /var/tmp/diff_new_pack.jeF2Fk/_old 2019-06-18 15:00:25.237267698 +0200
+++ /var/tmp/diff_new_pack.jeF2Fk/_new 2019-06-18 15:00:25.241267696 +0200
@@ -19,25 +19,19 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-PyChromecast
-Version: 2.5.2
+Version: 3.2.2
Release: 0
Summary: Python module to talk to Google Chromecast
License: MIT
Group: Development/Languages/Python
URL: https://github.com/balloob/pychromecast
Source:
https://files.pythonhosted.org/packages/source/P/PyChromecast/PyChromecast-%{version}.tar.gz
-BuildRequires: %{python_module casttube >= 0.1.0}
-BuildRequires: %{python_module protobuf >= 3.0.0}
-BuildRequires: %{python_module requests >= 2.0}
BuildRequires: %{python_module setuptools}
-BuildRequires: %{python_module six >= 1.10.0}
-BuildRequires: %{python_module zeroconf >= 0.17.7}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
Requires: python-casttube >= 0.1.0
Requires: python-protobuf >= 3.0.0
Requires: python-requests >= 2.0
-Requires: python-six >= 1.10.0
Requires: python-zeroconf >= 0.17.7
BuildArch: noarch
%python_subpackages
@@ -58,7 +52,7 @@
%install
%python_install
-%python_expand %fdupes -s %{buildroot}%{$python_sitelib}
+%python_expand %fdupes %{buildroot}%{$python_sitelib}
%files %{python_files}
%license LICENSE
++++++ PyChromecast-2.5.2.tar.gz -> PyChromecast-3.2.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/PKG-INFO
new/PyChromecast-3.2.2/PKG-INFO
--- old/PyChromecast-2.5.2/PKG-INFO 2019-02-16 20:45:19.000000000 +0100
+++ new/PyChromecast-3.2.2/PKG-INFO 2019-05-13 19:51:06.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: PyChromecast
-Version: 2.5.2
+Version: 3.2.2
Summary: Python module to talk to Google Chromecast.
Home-page: https://github.com/balloob/pychromecast
Author: Paulus Schoutsen
@@ -47,7 +47,7 @@
['Dev', 'Living Room', 'Den', 'Bedroom']
>> cast = next(cc for cc in chromecasts if cc.device.friendly_name
== "Living Room")
- >> # Wait for cast device to be ready
+ >> # Start worker thread and wait for cast device to be ready
>> cast.wait()
>> print(cast.device)
DeviceStatus(friendly_name='Living Room', model_name='Chromecast',
manufacturer='Google Inc.', uuid=UUID('df6944da-f016-4cb8-97d0-3da2ccaa380b'),
cast_type='cast')
@@ -142,13 +142,6 @@
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
------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/PyChromecast.egg-info/PKG-INFO
new/PyChromecast-3.2.2/PyChromecast.egg-info/PKG-INFO
--- old/PyChromecast-2.5.2/PyChromecast.egg-info/PKG-INFO 2019-02-16
20:45:19.000000000 +0100
+++ new/PyChromecast-3.2.2/PyChromecast.egg-info/PKG-INFO 2019-05-13
19:51:06.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: PyChromecast
-Version: 2.5.2
+Version: 3.2.2
Summary: Python module to talk to Google Chromecast.
Home-page: https://github.com/balloob/pychromecast
Author: Paulus Schoutsen
@@ -47,7 +47,7 @@
['Dev', 'Living Room', 'Den', 'Bedroom']
>> cast = next(cc for cc in chromecasts if cc.device.friendly_name
== "Living Room")
- >> # Wait for cast device to be ready
+ >> # Start worker thread and wait for cast device to be ready
>> cast.wait()
>> print(cast.device)
DeviceStatus(friendly_name='Living Room', model_name='Chromecast',
manufacturer='Google Inc.', uuid=UUID('df6944da-f016-4cb8-97d0-3da2ccaa380b'),
cast_type='cast')
@@ -142,13 +142,6 @@
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
------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/PyChromecast.egg-info/SOURCES.txt
new/PyChromecast-3.2.2/PyChromecast.egg-info/SOURCES.txt
--- old/PyChromecast-2.5.2/PyChromecast.egg-info/SOURCES.txt 2019-02-16
20:45:19.000000000 +0100
+++ new/PyChromecast-3.2.2/PyChromecast.egg-info/SOURCES.txt 2019-05-13
19:51:06.000000000 +0200
@@ -22,6 +22,7 @@
pychromecast/controllers/__init__.py
pychromecast/controllers/dashcast.py
pychromecast/controllers/media.py
+pychromecast/controllers/multizone.py
pychromecast/controllers/plex.py
pychromecast/controllers/spotify.py
pychromecast/controllers/youtube.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/README.rst
new/PyChromecast-3.2.2/README.rst
--- old/PyChromecast-2.5.2/README.rst 2018-07-10 10:42:56.000000000 +0200
+++ new/PyChromecast-3.2.2/README.rst 2019-04-01 20:36:19.000000000 +0200
@@ -39,7 +39,7 @@
['Dev', 'Living Room', 'Den', 'Bedroom']
>> cast = next(cc for cc in chromecasts if cc.device.friendly_name ==
"Living Room")
- >> # Wait for cast device to be ready
+ >> # Start worker thread and wait for cast device to be ready
>> cast.wait()
>> print(cast.device)
DeviceStatus(friendly_name='Living Room', model_name='Chromecast',
manufacturer='Google Inc.', uuid=UUID('df6944da-f016-4cb8-97d0-3da2ccaa380b'),
cast_type='cast')
@@ -134,13 +134,6 @@
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
------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/pychromecast/__init__.py
new/PyChromecast-3.2.2/pychromecast/__init__.py
--- old/PyChromecast-2.5.2/pychromecast/__init__.py 2019-02-12
20:29:47.000000000 +0100
+++ new/PyChromecast-3.2.2/pychromecast/__init__.py 2019-04-01
20:36:19.000000000 +0200
@@ -75,8 +75,6 @@
and returns a function which can be executed to stop
discovery.
- ex: get_chromecasts(friendly_name="Living Room")
-
May return an empty list if no chromecasts were found.
Tries is specified if you want to limit the number of times the
@@ -121,7 +119,7 @@
return internal_stop
-# pylint: disable=too-many-instance-attributes
+# pylint: disable=too-many-instance-attributes, too-many-public-methods
class Chromecast(object):
"""
Class to interface with a ChromeCast.
@@ -208,9 +206,6 @@
self.register_connection_listener = \
self.socket_client.register_connection_listener
- if blocking:
- self.socket_client.start()
-
@property
def ignore_cec(self):
""" Returns whether the CEC data should be ignored. """
@@ -322,6 +317,8 @@
Waits until the cast device is ready for communication. The device
is ready as soon a status message has been received.
+ If the worker thread is not already running, it will be started.
+
If the status has already been received then the method returns
immediately.
@@ -329,8 +326,17 @@
operation in seconds (or fractions thereof). Or None
to block forever.
"""
+ if not self.socket_client.isAlive():
+ self.socket_client.start()
self.status_event.wait(timeout=timeout)
+ def connect(self):
+ """ Connect to the chromecast.
+
+ Must only be called if the worker thread will not be started.
+ """
+ self.socket_client.connect()
+
def disconnect(self, timeout=None, blocking=True):
"""
Disconnects the chromecast and waits for it to terminate.
@@ -356,6 +362,12 @@
"""
self.socket_client.join(timeout=timeout)
+ def start(self):
+ """
+ Start the chromecast connection's worker thread.
+ """
+ self.socket_client.start()
+
def __del__(self):
try:
self.socket_client.stop.set()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/pychromecast/controllers/media.py
new/PyChromecast-3.2.2/pychromecast/controllers/media.py
--- old/PyChromecast-2.5.2/pychromecast/controllers/media.py 2018-07-10
10:42:56.000000000 +0200
+++ new/PyChromecast-3.2.2/pychromecast/controllers/media.py 2019-04-01
20:36:19.000000000 +0200
@@ -3,6 +3,7 @@
on the Chromecast.
"""
from datetime import datetime
+import logging
from collections import namedtuple
import threading
@@ -22,14 +23,16 @@
MESSAGE_TYPE = 'type'
+TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO"
TYPE_GET_STATUS = "GET_STATUS"
+TYPE_LOAD = "LOAD"
TYPE_MEDIA_STATUS = "MEDIA_STATUS"
-TYPE_PLAY = "PLAY"
TYPE_PAUSE = "PAUSE"
-TYPE_STOP = "STOP"
-TYPE_LOAD = "LOAD"
+TYPE_PLAY = "PLAY"
+TYPE_QUEUE_NEXT = "QUEUE_NEXT"
+TYPE_QUEUE_PREV = "QUEUE_PREV"
TYPE_SEEK = "SEEK"
-TYPE_EDIT_TRACKS_INFO = "EDIT_TRACKS_INFO"
+TYPE_STOP = "STOP"
METADATA_TYPE_GENERIC = 0
METADATA_TYPE_TVSHOW = 1
@@ -37,16 +40,37 @@
METADATA_TYPE_MUSICTRACK = 3
METADATA_TYPE_PHOTO = 4
+# From www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js
CMD_SUPPORT_PAUSE = 1
CMD_SUPPORT_SEEK = 2
CMD_SUPPORT_STREAM_VOLUME = 4
CMD_SUPPORT_STREAM_MUTE = 8
+# ALL_BASIC_MEDIA = PAUSE | SEEK | VOLUME | MUTE | EDIT_TRACKS | PLAYBACK_RATE
+CMD_SUPPORT_ALL_BASIC_MEDIA = 12303
+CMD_SUPPORT_QUEUE_NEXT = 64
+CMD_SUPPORT_QUEUE_PREV = 128
+CMD_SUPPORT_QUEUE_SHUFFLE = 256
+CMD_SUPPORT_QUEUE_REPEAT_ALL = 1024
+CMD_SUPPORT_QUEUE_REPEAT_ONE = 2048
+CMD_SUPPORT_QUEUE_REPEAT = 3072
+CMD_SUPPORT_SKIP_AD = 512
+CMD_SUPPORT_EDIT_TRACKS = 4096
+CMD_SUPPORT_PLAYBACK_RATE = 8192
+CMD_SUPPORT_LIKE = 16384
+CMD_SUPPORT_DISLIKE = 32768
+CMD_SUPPORT_FOLLOW = 65536
+CMD_SUPPORT_UNFOLLOW = 131072
+CMD_SUPPORT_STREAM_TRANSFER = 262144
+
+# Legacy?
CMD_SUPPORT_SKIP_FORWARD = 16
CMD_SUPPORT_SKIP_BACKWARD = 32
MediaImage = namedtuple('MediaImage', 'url height width')
+_LOGGER = logging.getLogger(__name__)
+
class MediaStatus(object):
""" Class to hold the media status. """
@@ -215,6 +239,16 @@
""" True if SKIP_BACKWARD is supported. """
return bool(self.supported_media_commands & CMD_SUPPORT_SKIP_BACKWARD)
+ @property
+ def supports_queue_next(self):
+ """ True if QUEUE_NEXT is supported. """
+ return bool(self.supported_media_commands & CMD_SUPPORT_QUEUE_NEXT)
+
+ @property
+ def supports_queue_prev(self):
+ """ True if QUEUE_PREV is supported. """
+ return bool(self.supported_media_commands & CMD_SUPPORT_QUEUE_PREV)
+
def update(self, data):
""" New data will only contain the changed attributes. """
if not data.get('status', []):
@@ -304,7 +338,7 @@
return False
def register_status_listener(self, listener):
- """ Register a listener for new media statusses. A new status will
+ """ Register a listener for new media statuses. A new status will
call listener.new_media_status(status) """
self._status_listeners.append(listener)
@@ -386,6 +420,14 @@
"currentTime": position,
"resumeState": "PLAYBACK_START"})
+ def queue_next(self):
+ """ Send the QUEUE_NEXT command. """
+ self._send_command({MESSAGE_TYPE: TYPE_QUEUE_NEXT})
+
+ def queue_prev(self):
+ """ Send the QUEUE_PREV command. """
+ self._send_command({MESSAGE_TYPE: TYPE_QUEUE_PREV})
+
def enable_subtitle(self, track_id):
""" Enable specific text track. """
self._send_command({
@@ -434,7 +476,8 @@
try:
listener.new_media_status(self.status)
except Exception: # pylint: disable=broad-except
- pass
+ _LOGGER.exception("Exception thrown when calling media status "
+ "callback")
# pylint: disable=too-many-arguments
def play_media(self, url, content_type, title=None, thumb=None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/PyChromecast-2.5.2/pychromecast/controllers/multizone.py
new/PyChromecast-3.2.2/pychromecast/controllers/multizone.py
--- old/PyChromecast-2.5.2/pychromecast/controllers/multizone.py
1970-01-01 01:00:00.000000000 +0100
+++ new/PyChromecast-3.2.2/pychromecast/controllers/multizone.py
2019-04-01 20:36:19.000000000 +0200
@@ -0,0 +1,253 @@
+"""
+Controller to monitor audio group members.
+"""
+import logging
+
+from . import BaseController
+from ..socket_client import (
+ CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED,
+ CONNECTION_STATUS_LOST)
+
+_LOGGER = logging.getLogger(__name__)
+
+MESSAGE_TYPE = "type"
+MULTIZONE_NAMESPACE = "urn:x-cast:com.google.cast.multizone"
+TYPE_CASTING_GROUPS = "CASTING_GROUPS"
+TYPE_DEVICE_ADDED = "DEVICE_ADDED"
+TYPE_DEVICE_UPDATED = "DEVICE_UPDATED"
+TYPE_DEVICE_REMOVED = "DEVICE_REMOVED"
+TYPE_GET_CASTING_GROUPS = "GET_CASTING_GROUPS"
+TYPE_GET_STATUS = "GET_STATUS"
+TYPE_MULTIZONE_STATUS = "MULTIZONE_STATUS"
+TYPE_SESSION_UPDATED = "PLAYBACK_SESSION_UPDATED"
+
+
+class Listener:
+ """ Callback handler. """
+ def __init__(self, group_cast, casts):
+ """Initialize the listener."""
+ self._casts = casts
+ group_cast.register_status_listener(self)
+ group_cast.media_controller.register_status_listener(self)
+ group_cast.register_connection_listener(self)
+ self._mz = MultizoneController(group_cast.uuid)
+ self._mz.register_listener(self)
+ self._group_uuid = str(group_cast.uuid)
+ group_cast.register_handler(self._mz)
+
+ def new_cast_status(self, cast_status):
+ """Handle reception of a new CastStatus."""
+ casts = self._casts
+ group_members = self._mz.members
+ for member_uuid in group_members:
+ if member_uuid not in casts:
+ continue
+ for listener in list(casts[member_uuid]['listeners']):
+ listener.multizone_new_cast_status(
+ self._group_uuid, cast_status)
+
+ def new_media_status(self, media_status):
+ """Handle reception of a new MediaStatus."""
+ casts = self._casts
+ group_members = self._mz.members
+ for member_uuid in group_members:
+ if member_uuid not in casts:
+ continue
+ for listener in list(casts[member_uuid]['listeners']):
+ listener.multizone_new_media_status(
+ self._group_uuid, media_status)
+
+ def new_connection_status(self, conn_status):
+ """Handle reception of a new ConnectionStatus."""
+ if conn_status.status == CONNECTION_STATUS_CONNECTED:
+ self._mz.update_members()
+ if (conn_status.status == CONNECTION_STATUS_DISCONNECTED or
+ conn_status.status == CONNECTION_STATUS_LOST):
+ self._mz.reset_members()
+
+ def multizone_member_added(self, member_uuid):
+ """Handle added audio group member."""
+ casts = self._casts
+ if member_uuid not in casts:
+ casts[member_uuid] = {'listeners': [],
+ 'groups': set()}
+ casts[member_uuid]['groups'].add(self._group_uuid)
+ for listener in list(casts[member_uuid]['listeners']):
+ listener.added_to_multizone(self._group_uuid)
+
+ def multizone_member_removed(self, member_uuid):
+ """Handle removed audio group member."""
+ casts = self._casts
+ if member_uuid not in casts:
+ casts[member_uuid] = {'listeners': [], 'groups': set()}
+ casts[member_uuid]['groups'].discard(self._group_uuid)
+ for listener in list(casts[member_uuid]['listeners']):
+ listener.removed_from_multizone(self._group_uuid)
+
+ def multizone_status_received(self):
+ """Handle reception of audio group status."""
+ pass
+
+
+class MultizoneManager:
+ """ Manage audio groups. """
+ def __init__(self):
+ # Protect self._casts because it will be accessed from callbacks from
+ # the casts' socket_client thread
+ self._casts = {}
+ self._groups = {}
+
+ def add_multizone(self, group_cast):
+ """ Start managing a group """
+ self._groups[str(group_cast.uuid)] = {
+ 'chromecast': group_cast,
+ 'listener': Listener(group_cast, self._casts),
+ 'members': set()}
+
+ def remove_multizone(self, group_uuid):
+ """ Stop managing a group """
+ group_uuid = str(group_uuid)
+ group = self._groups.pop(group_uuid, None)
+ # Inform all group members that they are no longer members
+ if group is not None:
+ group['listener']._mz.reset_members() # noqa: E501 pylint:
disable=protected-access
+ for member in self._casts.values():
+ member['groups'].discard(group_uuid)
+
+ def register_listener(self, member_uuid, listener):
+ """ Register a listener for audio group changes of cast uuid.
+ On update will call:
+ listener.added_to_multizone(group_uuid)
+ The cast has been added to group uuid
+ listener.removed_from_multizone(group_uuid)
+ The cast has been removed from group uuid
+ listener.multizone_new_media_status(group_uuid, media_status)
+ The group uuid, of which the cast is a member, has new status
+ listener.multizone_new_cast_status(group_uuid, cast_status)
+ The group uuid, of which the cast is a member, has new status
+ """
+ member_uuid = str(member_uuid)
+ if member_uuid not in self._casts:
+ self._casts[member_uuid] = {'listeners': [],
+ 'groups': set()}
+ self._casts[member_uuid]['listeners'].append(listener)
+
+ def deregister_listener(self, member_uuid, listener):
+ """ Deregister listener for audio group changes of cast uuid."""
+ self._casts[str(member_uuid)]['listeners'].remove(listener)
+
+ def get_multizone_memberships(self, member_uuid):
+ """ Return a list of audio groups in which cast member_uuid is a member
+ """
+ return list(self._casts[str(member_uuid)]['groups'])
+
+ def get_multizone_mediacontroller(self, group_uuid):
+ """ Get mediacontroller of a group """
+ return self._groups[str(group_uuid)]['chromecast'].media_controller
+
+
+class MultizoneController(BaseController):
+ """ Controller to monitor audio group members. """
+
+ def __init__(self, uuid):
+ self._members = {}
+ self._status_listeners = []
+ self._uuid = str(uuid)
+ super(MultizoneController, self).__init__(MULTIZONE_NAMESPACE,
+ target_platform=True)
+
+ def _add_member(self, uuid, name):
+ if uuid not in self._members:
+ self._members[uuid] = name
+ _LOGGER.debug("(%s) Added member %s(%s), members: %s",
+ self._uuid, uuid, name, self._members)
+ for listener in list(self._status_listeners):
+ listener.multizone_member_added(uuid)
+
+ def _remove_member(self, uuid):
+ name = self._members.pop(uuid, '<Unknown>')
+ _LOGGER.debug("(%s) Removed member %s(%s), members: %s",
+ self._uuid, uuid, name, self._members)
+ for listener in list(self._status_listeners):
+ listener.multizone_member_removed(uuid)
+
+ def register_listener(self, listener):
+ """ Register a listener for audio group changes. On update will call:
+ listener.multizone_member_added(uuid)
+ listener.multizone_member_removed(uuid)
+ listener.multizone_status_received()
+ """
+ self._status_listeners.append(listener)
+
+ @property
+ def members(self):
+ """ Return a list of audio group members. """
+ return list(self._members.keys())
+
+ def reset_members(self):
+ """ Reset audio group members. """
+ for uuid in list(self._members):
+ self._remove_member(uuid)
+
+ def update_members(self):
+ """ Update audio group members. """
+ self.send_message({MESSAGE_TYPE: TYPE_GET_STATUS})
+
+ def get_casting_groups(self):
+ """ Send GET_CASTING_GROUPS message. """
+ self.send_message({MESSAGE_TYPE: TYPE_GET_CASTING_GROUPS})
+
+ def receive_message(self, message, data): # noqa: E501 pylint:
disable=too-many-return-statements
+ """ Called when a multizone message is received. """
+ if data[MESSAGE_TYPE] == TYPE_DEVICE_ADDED:
+ uuid = data['device']['deviceId']
+ name = data['device']['name']
+ self._add_member(uuid, name)
+ return True
+
+ if data[MESSAGE_TYPE] == TYPE_DEVICE_REMOVED:
+ uuid = data['deviceId']
+ self._remove_member(uuid)
+ return True
+
+ if data[MESSAGE_TYPE] == TYPE_DEVICE_UPDATED:
+ uuid = data['device']['deviceId']
+ name = data['device']['name']
+ self._add_member(uuid, name)
+ return True
+
+ if data[MESSAGE_TYPE] == TYPE_MULTIZONE_STATUS:
+ members = data['status']['devices']
+ members = \
+ {member['deviceId']: member['name'] for member in members}
+ removed_members = \
+ list(set(self._members.keys())-set(members.keys()))
+ added_members = list(set(members.keys())-set(self._members.keys()))
+ _LOGGER.debug("(%s) Added members %s, Removed members: %s",
+ self._uuid, added_members, removed_members)
+
+ for uuid in removed_members:
+ self._remove_member(uuid)
+ for uuid in added_members:
+ self._add_member(uuid, members[uuid])
+
+ for listener in list(self._status_listeners):
+ listener.multizone_status_received()
+
+ return True
+
+ if data[MESSAGE_TYPE] == TYPE_SESSION_UPDATED:
+ # A temporary group has been formed
+ return True
+
+ if data[MESSAGE_TYPE] == TYPE_CASTING_GROUPS:
+ # Answer to GET_CASTING_GROUPS
+ return True
+
+ return False
+
+ def tear_down(self):
+ """ Called when controller is destroyed. """
+ super(MultizoneController, self).tear_down()
+
+ self._status_listeners[:] = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/PyChromecast-2.5.2/pychromecast/controllers/spotify.py
new/PyChromecast-3.2.2/pychromecast/controllers/spotify.py
--- old/PyChromecast-2.5.2/pychromecast/controllers/spotify.py 2018-07-10
10:42:56.000000000 +0200
+++ new/PyChromecast-3.2.2/pychromecast/controllers/spotify.py 2019-05-13
19:47:11.000000000 +0200
@@ -2,14 +2,18 @@
Controller to interface with Spotify.
"""
import logging
-import time
+import threading
from . import BaseController
from ..config import APP_SPOTIFY
+from ..error import LaunchError
APP_NAMESPACE = "urn:x-cast:com.spotify.chromecast.secure.v1"
-TYPE_STATUS = "setCredentials"
-TYPE_RESPONSE_STATUS = 'setCredentialsResponse'
+TYPE_GET_INFO = "getInfo"
+TYPE_GET_INFO_RESPONSE = "getInfoResponse"
+TYPE_SET_CREDENTIALS = "setCredentials"
+TYPE_SET_CREDENTIALS_ERROR = 'setCredentialsError'
+TYPE_SET_CREDENTIALS_RESPONSE = 'setCredentialsResponse'
# pylint: disable=too-many-instance-attributes
@@ -19,32 +23,58 @@
# pylint: disable=useless-super-delegation
# The pylint rule useless-super-delegation doesn't realize
# we are setting default values here.
- def __init__(self, access_token):
+ def __init__(self, access_token, expires):
super(SpotifyController, self).__init__(APP_NAMESPACE, APP_SPOTIFY)
+ if access_token is None or expires is None:
+ raise ValueError("access_token and expires cannot be empty")
self.logger = logging.getLogger(__name__)
self.session_started = False
self.access_token = access_token
+ self.expires = expires
self.is_launched = False
+ self.device = None
+ self.credential_error = False
+ self.waiting = threading.Event()
# pylint: enable=useless-super-delegation
# pylint: disable=unused-argument,no-self-use
def receive_message(self, message, data):
- """ Currently not doing anything with received messages. """
- if data['type'] == TYPE_RESPONSE_STATUS:
+ """ Handle the auth flow and active player selection """
+ if data['type'] == TYPE_SET_CREDENTIALS_RESPONSE:
+ self.send_message({'type': TYPE_GET_INFO, 'payload': {}})
+ if data['type'] == TYPE_SET_CREDENTIALS_ERROR:
+ self.device = None
+ self.credential_error = True
+ self.waiting.set()
+ if data['type'] == TYPE_GET_INFO_RESPONSE:
+ self.device = data['payload']['deviceID']
self.is_launched = True
+ self.waiting.set()
return True
- def launch_app(self):
- """ Launch main application """
+ def launch_app(self, timeout=10):
+ """
+ Launch Spotify application.
+
+ Will raise a LaunchError exception if there is no response from the
+ Spotify app within timeout seconds.
+ """
def callback():
"""Callback function"""
- self.send_message({"type": TYPE_STATUS,
- "credentials": self.access_token})
-
+ self.send_message({"type": TYPE_SET_CREDENTIALS,
+ "credentials": self.access_token,
+ "expiresIn": self.expires})
+
+ self.device = None
+ self.credential_error = False
+ self.waiting.clear()
self.launch(callback_function=callback)
# Need to wait for Spotify to be launched on Chromecast completely
- while not self.is_launched:
- time.sleep(1)
+ self.waiting.wait(timeout)
+
+ if not self.is_launched:
+ raise LaunchError(
+ "Timeout when waiting for status response from Spotify app")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/pychromecast/dial.py
new/PyChromecast-3.2.2/pychromecast/dial.py
--- old/PyChromecast-2.5.2/pychromecast/dial.py 2019-02-15 18:43:54.000000000
+0100
+++ new/PyChromecast-3.2.2/pychromecast/dial.py 2019-04-27 12:39:22.000000000
+0200
@@ -28,6 +28,7 @@
'eureka dongle': CAST_TYPE_CHROMECAST,
'chromecast audio': CAST_TYPE_AUDIO,
'google home': CAST_TYPE_AUDIO,
+ 'google home mini': CAST_TYPE_AUDIO,
'google cast group': CAST_TYPE_GROUP,
}
@@ -40,6 +41,38 @@
data='{"params":"now"}', timeout=10)
+def _get_status(host, services, zconf, path):
+ """
+ :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
+ """
+
+ if not host:
+ for service in services.copy():
+ service_info = get_info_from_service(service, zconf)
+ host, _ = get_host_from_service_info(service_info)
+ if host:
+ _LOGGER.debug("Resolved service %s to %s", service, host)
+ break
+
+ req = CC_SESSION.get(FORMAT_BASE_URL.format(host) + path, timeout=10)
+
+ req.raise_for_status()
+
+ # 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'
+
+ return req.json()
+
+
def get_device_status(host, services=None, zconf=None):
"""
:param host: Hostname or ip to fetch status from
@@ -49,30 +82,8 @@
"""
try:
- if not host:
- for service in services.copy():
- service_info = get_info_from_service(service, zconf)
- host, _ = get_host_from_service_info(service_info)
- if host:
- _LOGGER.debug("Resolved service %s to %s", service, host)
- break
-
- req = CC_SESSION.get(
- FORMAT_BASE_URL.format(host) + "/setup/eureka_info?options=detail",
- timeout=10)
-
- req.raise_for_status()
-
- # 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 = req.json()
+ status = _get_status(
+ host, services, zconf, "/setup/eureka_info?options=detail")
friendly_name = status.get('name', "Unknown Chromecast")
model_name = "Unknown model name"
@@ -97,6 +108,52 @@
return None
+def get_multizone_status(host, services=None, zconf=None):
+ """
+ :param host: Hostname or ip to fetch status from
+ :type host: str
+ :return: The multizone status as a named tuple.
+ :rtype: pychromecast.dial.MultizoneStatus or None
+ """
+
+ try:
+ status = status = _get_status(
+ host, services, zconf, "/setup/eureka_info?params=multizone")
+
+ dynamic_groups = []
+ if 'multizone' in status and 'dynamic_groups' in status['multizone']:
+ for group in status['multizone']['dynamic_groups']:
+ name = group.get('name', "Unknown group name")
+ udn = group.get('uuid', None)
+ uuid = None
+ if udn:
+ uuid = UUID(udn.replace('-', ''))
+ dynamic_groups.append(MultizoneInfo(name, uuid))
+
+ groups = []
+ if 'multizone' in status and 'groups' in status['multizone']:
+ for group in status['multizone']['groups']:
+ name = group.get('name', "Unknown group name")
+ udn = group.get('uuid', None)
+ uuid = None
+ if udn:
+ uuid = UUID(udn.replace('-', ''))
+ groups.append(MultizoneInfo(name, uuid))
+
+ return MultizoneStatus(dynamic_groups, groups)
+
+ except (requests.exceptions.RequestException, OSError, ValueError):
+ return None
+
+
DeviceStatus = namedtuple(
"DeviceStatus",
["friendly_name", "model_name", "manufacturer", "uuid", "cast_type"])
+
+MultizoneInfo = namedtuple(
+ "MultizoneInfo",
+ ["friendly_name", "uuid"])
+
+MultizoneStatus = namedtuple(
+ "MultizoneStatus",
+ ["dynamic_groups", "groups"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/pychromecast/socket_client.py
new/PyChromecast-3.2.2/pychromecast/socket_client.py
--- old/PyChromecast-2.5.2/pychromecast/socket_client.py 2019-02-16
20:43:30.000000000 +0100
+++ new/PyChromecast-3.2.2/pychromecast/socket_client.py 2019-04-27
12:39:22.000000000 +0200
@@ -212,14 +212,6 @@
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): # noqa: E501
pylint:disable=too-many-statements, too-many-branches
"""Initialize a socket to a Chromecast, retrying as necessary."""
tries = self.tries
@@ -361,6 +353,19 @@
self.fn or self.host, self.port)
raise ChromecastConnectionError("Failed to connect")
+ def connect(self):
+ """ Connect socket connection to Chromecast device.
+
+ Must only be called if the worker thread will not be started.
+ """
+ try:
+ self.initialize_connection()
+ except ChromecastConnectionError:
+ self._report_connection_status(
+ ConnectionStatus(CONNECTION_STATUS_DISCONNECTED,
+ NetworkAddress(self.host, self.port)))
+ return
+
def disconnect(self):
""" Disconnect socket connection to Chromecast device """
self.stop.set()
@@ -413,7 +418,15 @@
return self.stop.is_set()
def run(self):
- """ Start polling the socket. """
+ """ Connect to the cast and start polling the socket. """
+ try:
+ self.initialize_connection()
+ except ChromecastConnectionError:
+ self._report_connection_status(
+ ConnectionStatus(CONNECTION_STATUS_DISCONNECTED,
+ NetworkAddress(self.host, self.port)))
+ return
+
self.heartbeat_controller.reset()
self._force_recon = False
logging.debug("Thread started...")
@@ -522,6 +535,8 @@
reset = True
if reset:
+ for channel in self._open_channels:
+ self.disconnect_channel(channel)
self._report_connection_status(
ConnectionStatus(CONNECTION_STATUS_LOST,
NetworkAddress(self.host, self.port)))
@@ -601,7 +616,9 @@
id(listener), type(listener).__name__)
listener.new_connection_status(status)
except Exception: # pylint: disable=broad-except
- pass
+ self.logger.exception(
+ "[%s:%s] Exception thrown when calling connection "
+ "listener", self.fn or self.host, self.port)
def _read_bytes_from_socket(self, msglen):
""" Read bytes from the socket. """
@@ -698,8 +715,8 @@
self.logger.info('[%s:%s] Error writing to socket.',
self.fn or self.host, self.port)
else:
- raise NotConnected("Chromecast " + self.host + ":" + self.port +
- " is connecting...")
+ raise NotConnected("Chromecast " + self.host + ":" +
+ str(self.port) + " is connecting...")
def send_platform_message(self, namespace, message, inc_session_id=False,
callback_function_param=False):
@@ -747,10 +764,16 @@
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)
+ try:
+ self.send_message(
+ destination_id, NS_CONNECTION,
+ {MESSAGE_TYPE: TYPE_CLOSE, 'origin': {}},
+ no_add_request_id=True, force=True)
+ except NotConnected:
+ pass
+ except Exception: # pylint: disable=broad-except
+ self.logger.exception("[%s:%s] Exception",
+ self.fn or self.host, self.port)
self._open_channels.remove(destination_id)
@@ -1024,7 +1047,8 @@
try:
listener.new_cast_status(self.status)
except Exception: # pylint: disable=broad-except
- pass
+ self.logger.exception(
+ "Exception thrown when calling cast status listener")
@staticmethod
def _parse_launch_error(data):
@@ -1057,7 +1081,8 @@
try:
listener.new_launch_error(launch_failure)
except Exception: # pylint: disable=broad-except
- pass
+ self.logger.exception(
+ "Exception thrown when calling launch error listener")
def tear_down(self):
""" Called when controller is destroyed. """
@@ -1067,7 +1092,6 @@
self.launch_failure = None
self.app_to_launch = None
self.app_launch_event.clear()
- self._report_status()
self._status_listeners[:] = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-2.5.2/setup.py
new/PyChromecast-3.2.2/setup.py
--- old/PyChromecast-2.5.2/setup.py 2019-02-16 20:43:34.000000000 +0100
+++ new/PyChromecast-3.2.2/setup.py 2019-05-13 19:46:26.000000000 +0200
@@ -5,7 +5,7 @@
setup(
name='PyChromecast',
- version='2.5.2',
+ version='3.2.2',
license='MIT',
url='https://github.com/balloob/pychromecast',
author='Paulus Schoutsen',