From 141ba681d1a98dad28c1d2927e370394a571495f Mon Sep 17 00:00:00 2001 From: Andreas Harter Date: Mon, 1 Oct 2018 19:31:34 +0200 Subject: [PATCH 01/15] Upgrade to python3, disable chromecast support --- man/pulseaudio-dlna.1 | 2 +- pulseaudio_dlna/__init__.py | 6 +-- pulseaudio_dlna/__main__.py | 4 +- pulseaudio_dlna/application.py | 19 +++++---- pulseaudio_dlna/codecs.py | 10 ++--- pulseaudio_dlna/covermodes.py | 4 +- pulseaudio_dlna/daemon.py | 4 +- pulseaudio_dlna/encoders/__init__.py | 20 +++++----- pulseaudio_dlna/encoders/avconv.py | 4 +- pulseaudio_dlna/encoders/ffmpeg.py | 4 +- pulseaudio_dlna/encoders/generic.py | 4 +- pulseaudio_dlna/holder.py | 6 +-- pulseaudio_dlna/images.py | 6 +-- pulseaudio_dlna/notification.py | 4 +- pulseaudio_dlna/plugins/__init__.py | 4 +- .../plugins/chromecast/__init__.py | 4 +- pulseaudio_dlna/plugins/chromecast/mdns.py | 4 +- .../plugins/chromecast/pycastv2/__init__.py | 8 ++-- .../chromecast/pycastv2/cast_channel_pb2.py | 23 +++++------ .../chromecast/pycastv2/cast_socket.py | 6 +-- .../plugins/chromecast/pycastv2/commands.py | 4 +- .../plugins/chromecast/pycastv2/example.py | 6 +-- .../plugins/chromecast/renderer.py | 23 +++++------ pulseaudio_dlna/plugins/dlna/__init__.py | 4 +- .../plugins/dlna/pyupnpv2/__init__.py | 40 +++++++++---------- pulseaudio_dlna/plugins/dlna/pyupnpv2/byto.py | 4 +- pulseaudio_dlna/plugins/dlna/renderer.py | 9 ++--- pulseaudio_dlna/plugins/dlna/ssdp/__init__.py | 6 +-- pulseaudio_dlna/plugins/dlna/ssdp/discover.py | 6 +-- pulseaudio_dlna/plugins/dlna/ssdp/listener.py | 14 +++---- pulseaudio_dlna/plugins/renderer.py | 20 +++++----- pulseaudio_dlna/pulseaudio.py | 36 ++++++++--------- pulseaudio_dlna/recorders.py | 4 +- pulseaudio_dlna/rules.py | 14 +++---- pulseaudio_dlna/streamserver.py | 37 ++++++++--------- pulseaudio_dlna/utils/encoding.py | 28 ++++++------- pulseaudio_dlna/utils/git.py | 4 +- pulseaudio_dlna/utils/network.py | 4 +- pulseaudio_dlna/utils/psutil.py | 6 +-- pulseaudio_dlna/utils/subprocess.py | 6 +-- pulseaudio_dlna/workarounds.py | 8 ++-- scripts/chromecast-beam.py | 24 +++++------ scripts/fritzbox-device-sharing.py | 10 ++--- scripts/radio.py | 12 +++--- setup.py | 8 ++-- 45 files changed, 237 insertions(+), 246 deletions(-) diff --git a/man/pulseaudio-dlna.1 b/man/pulseaudio-dlna.1 index afbc4d24..52ce3b52 100644 --- a/man/pulseaudio-dlna.1 +++ b/man/pulseaudio-dlna.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.2. -.TH PULSEAUDIO-DLNA "1" "April 2016" "pulseaudio-dlna 0.5.2" "User Commands" +.TH PULSEAUDIO-DLNA "1" "April 2016" "pulseaudio-dlna 0.6.0" "User Commands" .SH NAME pulseaudio-dlna \- Stream audio to DLNA devices and Chromecasts .SH DESCRIPTION diff --git a/pulseaudio_dlna/__init__.py b/pulseaudio_dlna/__init__.py index 427d5a79..03a8080b 100644 --- a/pulseaudio_dlna/__init__.py +++ b/pulseaudio_dlna/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,12 +15,12 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import os import pkg_resources -import utils.git +from .utils import git try: version = pkg_resources.get_distribution(__package__).version diff --git a/pulseaudio_dlna/__main__.py b/pulseaudio_dlna/__main__.py index f49941bf..a5748a7e 100644 --- a/pulseaudio_dlna/__main__.py +++ b/pulseaudio_dlna/__main__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -116,7 +116,7 @@ ''' -from __future__ import unicode_literals + import sys import os diff --git a/pulseaudio_dlna/application.py b/pulseaudio_dlna/application.py index bca5a10a..50aac5f1 100644 --- a/pulseaudio_dlna/application.py +++ b/pulseaudio_dlna/application.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import multiprocessing import signal @@ -32,8 +32,8 @@ import pulseaudio_dlna.plugins.dlna.ssdp import pulseaudio_dlna.plugins.dlna.ssdp.listener import pulseaudio_dlna.plugins.dlna.ssdp.discover -import pulseaudio_dlna.plugins.chromecast -import pulseaudio_dlna.plugins.chromecast.mdns +# import pulseaudio_dlna.plugins.chromecast +# import pulseaudio_dlna.plugins.chromecast.mdns import pulseaudio_dlna.encoders import pulseaudio_dlna.covermodes import pulseaudio_dlna.streamserver @@ -47,7 +47,6 @@ class Application(object): - ENCODING = 'utf-8' DEVICE_CONFIG_PATHS = [ os.path.expanduser('~/.local/share/pulseaudio-dlna'), '/etc/pulseaudio-dlna', @@ -55,7 +54,7 @@ class Application(object): DEVICE_CONFIG = 'devices.json' PLUGINS = [ pulseaudio_dlna.plugins.dlna.DLNAPlugin(), - pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), + # pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), ] SHUTDOWN_TIMEOUT = 5 @@ -213,7 +212,7 @@ def run(self, options): logger.info(' {}'.format(encoder)) logger.info('Codec settings:') - for identifier, _type in pulseaudio_dlna.codecs.CODECS.iteritems(): + for identifier, _type in pulseaudio_dlna.codecs.CODECS.items(): codec = _type() logger.info(' {}'.format(codec)) @@ -331,9 +330,9 @@ def obj_to_dict(obj): continue try: with open(config_file, 'w') as h: - h.write(json_text.encode(self.ENCODING)) + h.write(json_text) logger.info('Found the following devices:') - for device in holder.devices.values(): + for device in list(holder.devices.values()): logger.info('{name} ({flavour})'.format( name=device.name, flavour=device.flavour)) for codec in device.codecs: @@ -356,7 +355,7 @@ def read_device_config(self): if os.path.isfile(config_file) and \ os.access(config_file, os.R_OK): with open(config_file, 'r') as h: - json_text = h.read().decode(self.ENCODING) + json_text = h.read() logger.debug('Device configuration:\n{}'.format(json_text)) json_text = json_text.replace('\n', '') try: diff --git a/pulseaudio_dlna/codecs.py b/pulseaudio_dlna/codecs.py index cb671cb5..f4504301 100644 --- a/pulseaudio_dlna/codecs.py +++ b/pulseaudio_dlna/codecs.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import functools import logging @@ -67,7 +67,7 @@ def set_backend(backend): def set_codecs(identifiers): step = 3 priority = (len(CODECS) + 1) * step - for identifier, _type in CODECS.iteritems(): + for identifier, _type in CODECS.items(): _type.ENABLED = False _type.PRIORITY = 0 for identifier in identifiers: @@ -81,7 +81,7 @@ def set_codecs(identifiers): def enabled_codecs(): codecs = [] - for identifier, _type in CODECS.iteritems(): + for identifier, _type in CODECS.items(): if _type.ENABLED: codecs.append(_type()) return codecs @@ -168,7 +168,7 @@ def __str__(self, detailed=False): def to_json(self): attributes = ['priority', 'suffix', 'mime_type'] d = { - k: v for k, v in self.__dict__.iteritems() + k: v for k, v in iter(self.__dict__.items()) if k not in attributes } d['mime_type'] = self.specific_mime_type diff --git a/pulseaudio_dlna/covermodes.py b/pulseaudio_dlna/covermodes.py index d3dbd905..d7deb770 100644 --- a/pulseaudio_dlna/covermodes.py +++ b/pulseaudio_dlna/covermodes.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import sys import inspect diff --git a/pulseaudio_dlna/daemon.py b/pulseaudio_dlna/daemon.py index 3f9e7cda..13ef28e1 100644 --- a/pulseaudio_dlna/daemon.py +++ b/pulseaudio_dlna/daemon.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + from gi.repository import GObject diff --git a/pulseaudio_dlna/encoders/__init__.py b/pulseaudio_dlna/encoders/__init__.py index 667e15fe..f4f57144 100644 --- a/pulseaudio_dlna/encoders/__init__.py +++ b/pulseaudio_dlna/encoders/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import distutils.spawn import inspect @@ -67,9 +67,7 @@ def _find_executable(path): # therefore not capable of handling unicode properly when it contains # non-ascii characters. encoding = 'utf-8' - result = distutils.spawn.find_executable(path.encode(encoding)) - if result is not None and type(result) is str: - result = result.decode(encoding) + result = distutils.spawn.find_executable(path) return result @@ -113,7 +111,7 @@ def supported_bit_rates(self): def __str__(self): return '<{} available="{}">'.format( self.__class__.__name__, - unicode(self.available), + str(self.available), ) @@ -139,8 +137,8 @@ def supported_bit_rates(self): def __str__(self): return '<{} available="{}" bit-rate="{}">'.format( self.__class__.__name__, - unicode(self.available), - unicode(self.bit_rate), + str(self.available), + str(self.bit_rate), ) @@ -165,9 +163,9 @@ def channels(self, value): def __str__(self): return '<{} available="{}" sample-rate="{}" channels="{}">'.format( self.__class__.__name__, - unicode(self.available), - unicode(self.sample_rate), - unicode(self.channels), + str(self.available), + str(self.sample_rate), + str(self.channels), ) diff --git a/pulseaudio_dlna/encoders/avconv.py b/pulseaudio_dlna/encoders/avconv.py index 5f09b25a..b19c5070 100644 --- a/pulseaudio_dlna/encoders/avconv.py +++ b/pulseaudio_dlna/encoders/avconv.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging diff --git a/pulseaudio_dlna/encoders/ffmpeg.py b/pulseaudio_dlna/encoders/ffmpeg.py index c16b2a4b..ed94c22a 100644 --- a/pulseaudio_dlna/encoders/ffmpeg.py +++ b/pulseaudio_dlna/encoders/ffmpeg.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging diff --git a/pulseaudio_dlna/encoders/generic.py b/pulseaudio_dlna/encoders/generic.py index e68e1267..14e174a9 100644 --- a/pulseaudio_dlna/encoders/generic.py +++ b/pulseaudio_dlna/encoders/generic.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging diff --git a/pulseaudio_dlna/holder.py b/pulseaudio_dlna/holder.py index 69e061be..ded44dd7 100644 --- a/pulseaudio_dlna/holder.py +++ b/pulseaudio_dlna/holder.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging import threading @@ -96,7 +96,7 @@ def lookup(self, locations): 'Connection refused.'.format(url=url)) for plugin in self.plugins: - for url, xml in xmls.items(): + for url, xml in list(xmls.items()): device = plugin.lookup(url, xml) self.add_device(device) diff --git a/pulseaudio_dlna/images.py b/pulseaudio_dlna/images.py index 40a46349..0a453f80 100644 --- a/pulseaudio_dlna/images.py +++ b/pulseaudio_dlna/images.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,8 +15,8 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals -from __future__ import with_statement + + import tempfile import logging diff --git a/pulseaudio_dlna/notification.py b/pulseaudio_dlna/notification.py index d270c4e7..6de13ca2 100644 --- a/pulseaudio_dlna/notification.py +++ b/pulseaudio_dlna/notification.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging diff --git a/pulseaudio_dlna/plugins/__init__.py b/pulseaudio_dlna/plugins/__init__.py index 98f2252c..962c6c6a 100644 --- a/pulseaudio_dlna/plugins/__init__.py +++ b/pulseaudio_dlna/plugins/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import functools diff --git a/pulseaudio_dlna/plugins/chromecast/__init__.py b/pulseaudio_dlna/plugins/chromecast/__init__.py index adf8318d..09bc5533 100644 --- a/pulseaudio_dlna/plugins/chromecast/__init__.py +++ b/pulseaudio_dlna/plugins/chromecast/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging diff --git a/pulseaudio_dlna/plugins/chromecast/mdns.py b/pulseaudio_dlna/plugins/chromecast/mdns.py index 625e3c58..a7d56c43 100644 --- a/pulseaudio_dlna/plugins/chromecast/mdns.py +++ b/pulseaudio_dlna/plugins/chromecast/mdns.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + from gi.repository import GObject diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py index cd6fbce4..5d3f4357 100644 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py +++ b/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,13 +15,13 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import time import logging -import commands -import cast_socket +from . import commands +from . import cast_socket logger = logging.getLogger('pycastv2') diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_channel_pb2.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_channel_pb2.py index b9b084de..ee69a102 100644 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_channel_pb2.py +++ b/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_channel_pb2.py @@ -94,21 +94,21 @@ _descriptor.FieldDescriptor( name='source_id', full_name='extensions.api.cast_channel.CastMessage.source_id', index=1, number=2, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=unicode("", "utf-8"), + has_default_value=False, default_value=bytes("", "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.api.cast_channel.CastMessage.destination_id', index=2, number=3, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=unicode("", "utf-8"), + has_default_value=False, default_value=bytes("", "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.api.cast_channel.CastMessage.namespace', index=3, number=4, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=unicode("", "utf-8"), + has_default_value=False, default_value=bytes("", "utf-8"), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), @@ -122,7 +122,7 @@ _descriptor.FieldDescriptor( name='payload_utf8', full_name='extensions.api.cast_channel.CastMessage.payload_utf8', index=5, number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=unicode("", "utf-8"), + has_default_value=False, default_value=bytes("", "utf-8"), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, options=None), @@ -290,32 +290,27 @@ DESCRIPTOR.message_types_by_name['AuthError'] = _AUTHERROR DESCRIPTOR.message_types_by_name['DeviceAuthMessage'] = _DEVICEAUTHMESSAGE -class CastMessage(_message.Message): - __metaclass__ = _reflection.GeneratedProtocolMessageType +class CastMessage(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): DESCRIPTOR = _CASTMESSAGE # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.CastMessage) -class AuthChallenge(_message.Message): - __metaclass__ = _reflection.GeneratedProtocolMessageType +class AuthChallenge(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): DESCRIPTOR = _AUTHCHALLENGE # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthChallenge) -class AuthResponse(_message.Message): - __metaclass__ = _reflection.GeneratedProtocolMessageType +class AuthResponse(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): DESCRIPTOR = _AUTHRESPONSE # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthResponse) -class AuthError(_message.Message): - __metaclass__ = _reflection.GeneratedProtocolMessageType +class AuthError(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): DESCRIPTOR = _AUTHERROR # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthError) -class DeviceAuthMessage(_message.Message): - __metaclass__ = _reflection.GeneratedProtocolMessageType +class DeviceAuthMessage(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): DESCRIPTOR = _DEVICEAUTHMESSAGE # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.DeviceAuthMessage) diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py index fdcc6cd3..04e8ef52 100644 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py +++ b/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import ssl import socket @@ -26,7 +26,7 @@ import traceback import select -import cast_channel_pb2 +from . import cast_channel_pb2 logger = logging.getLogger('pycastv2.cast_socket') diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/commands.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/commands.py index e9bc9e43..efb4dd95 100644 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/commands.py +++ b/pulseaudio_dlna/plugins/chromecast/pycastv2/commands.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + class BaseCommand(object): diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/example.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/example.py index ebb296d3..a8caaff4 100644 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/example.py +++ b/pulseaudio_dlna/plugins/chromecast/pycastv2/example.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,10 +15,10 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging -import __init__ as pycastv2 +from . import __init__ as pycastv2 logging.basicConfig(level=logging.DEBUG) diff --git a/pulseaudio_dlna/plugins/chromecast/renderer.py b/pulseaudio_dlna/plugins/chromecast/renderer.py index fc051bbe..e796b511 100644 --- a/pulseaudio_dlna/plugins/chromecast/renderer.py +++ b/pulseaudio_dlna/plugins/chromecast/renderer.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,16 +15,16 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import requests import logging -import urlparse +import urllib.parse import socket import traceback import lxml -import pycastv2 +from . import pycastv2 import pulseaudio_dlna.plugins.renderer import pulseaudio_dlna.rules import pulseaudio_dlna.codecs @@ -101,8 +101,7 @@ def play(self, url=None, codec=None, artist=None, title=None, thumb=None): traceback.print_exc() return 500, None except (pulseaudio_dlna.plugins.renderer.NoEncoderFoundException, - pulseaudio_dlna.plugins.renderer.NoSuitableHostFoundException)\ - as e: + pulseaudio_dlna.plugins.renderer.NoSuitableHostFoundException) as e: return 500, e except Exception: traceback.print_exc() @@ -180,7 +179,7 @@ def from_url(cls, url): @classmethod def from_xml(cls, url, xml): - url_object = urlparse.urlparse(url) + url_object = urllib.parse.urlparse(url) ip, port = url_object.netloc.split(':') try: xml_root = lxml.etree.fromstring(xml) @@ -202,14 +201,14 @@ def from_xml(cls, url, xml): return None return ChromecastRenderer( - name=unicode(device_friendlyname.text), - ip=unicode(ip), + name=str(device_friendlyname.text), + ip=str(ip), port=None, - udn=unicode(device_udn.text), - model_name=unicode(device_modelname.text), + udn=str(device_udn.text), + model_name=str(device_modelname.text), model_number=None, model_description=None, - manufacturer=unicode(device_manufacturer.text), + manufacturer=str(device_manufacturer.text), ) except: logger.error('No valid XML returned from {url}.'.format(url=url)) diff --git a/pulseaudio_dlna/plugins/dlna/__init__.py b/pulseaudio_dlna/plugins/dlna/__init__.py index fe5ca2d3..42b39e7f 100644 --- a/pulseaudio_dlna/plugins/dlna/__init__.py +++ b/pulseaudio_dlna/plugins/dlna/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging import threading diff --git a/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py b/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py index 6fb0ea0c..92bc9cd9 100644 --- a/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py +++ b/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,16 +15,16 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import requests -import urlparse +import urllib.parse import logging import collections import lxml import lxml.builder -import byto +from . import byto logger = logging.getLogger('pyupnpv2') @@ -138,16 +138,16 @@ def etree_to_dict(t): if children: dd = defaultdict(list) for dc in map(etree_to_dict, children): - for k, v in dc.items(): + for k, v in list(dc.items()): dd[k].append(v) d = { _tag_name(t): { - k: v[0] if len(v) == 1 else v for k, v in dd.items() + k: v[0] if len(v) == 1 else v for k, v in list(dd.items()) } } if t.attrib: d[_tag_name(t)].update( - ('@' + k, v) for k, v in t.attrib.items() + ('@' + k, v) for k, v in list(t.attrib.items()) ) if t.text: text = t.text.strip() @@ -261,7 +261,7 @@ def _generate_soap_xml( xml_declaration=True, pretty_print=False, encoding='utf-8'): def _add_dict(root, dict_): - for tag, value in dict_.items(): + for tag, value in list(dict_.items()): if isinstance(value, dict): element = lxml.etree.Element(tag) _add_dict(element, value) @@ -336,7 +336,7 @@ def _do_post_request(self, url, headers, data): try: response = None response = self._request.post( - url, data=data.encode(self.ENCODING), headers=headers, + url, data=data, headers=headers, timeout=self.TIMEOUT) return response finally: @@ -386,7 +386,7 @@ def control_url(self): ip=self.ip, port=self.port, ) - return urlparse.urljoin(host, self._control_url) + return urllib.parse.urljoin(host, self._control_url) @property def event_url(self): @@ -394,7 +394,7 @@ def event_url(self): ip=self.ip, port=self.port, ) - return urlparse.urljoin(host, self._event_url) + return urllib.parse.urljoin(host, self._event_url) @property def scpd_url(self): @@ -402,7 +402,7 @@ def scpd_url(self): ip=self.ip, port=self.port, ) - return urlparse.urljoin(host, self._scpd_url) + return urllib.parse.urljoin(host, self._scpd_url) class UpnpAVTransportService(UpnpService): @@ -642,7 +642,7 @@ def from_url(cls, url): def from_xml(cls, url, xml): def process_xml(url, xml_root, xml): - url_object = urlparse.urlparse(url) + url_object = urllib.parse.urlparse(url) ip, port = url_object.netloc.split(':') services = [] for device in xml_root.findall('.//{*}device'): @@ -671,20 +671,20 @@ def process_xml(url, xml_root, xml): upnp_device = UpnpMediaRenderer( description_xml=xml, access_url=url, - ip=unicode(ip), + ip=str(ip), port=port, - name=unicode(device_friendlyname.text), - udn=unicode(device_udn.text), - model_name=unicode( + name=str(device_friendlyname.text), + udn=str(device_udn.text), + model_name=str( device_modelname.text) if ( device_modelname is not None) else None, - model_number=unicode( + model_number=str( device_modelnumber.text) if ( device_modelnumber is not None) else None, - model_description=unicode( + model_description=str( device_modeldescription.text) if ( device_modeldescription is not None) else None, - manufacturer=unicode( + manufacturer=str( device_manufacturer.text) if ( device_manufacturer is not None) else None, services=services, diff --git a/pulseaudio_dlna/plugins/dlna/pyupnpv2/byto.py b/pulseaudio_dlna/plugins/dlna/pyupnpv2/byto.py index c4ca9070..a5106dbb 100644 --- a/pulseaudio_dlna/plugins/dlna/pyupnpv2/byto.py +++ b/pulseaudio_dlna/plugins/dlna/pyupnpv2/byto.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -17,7 +17,7 @@ """A module which runs things without importing unicode_literals -Sometimes you want pythons builtin functions just to run on raw bytes. Since +Sometimes you want python3s builtin functions just to run on raw bytes. Since the unicode_literals module changes that behavior for many string manipulations this module is a workarounds for not using future.utils.bytes_to_native_str method. diff --git a/pulseaudio_dlna/plugins/dlna/renderer.py b/pulseaudio_dlna/plugins/dlna/renderer.py index b695e8d5..39d4a6d8 100644 --- a/pulseaudio_dlna/plugins/dlna/renderer.py +++ b/pulseaudio_dlna/plugins/dlna/renderer.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging import time @@ -27,7 +27,7 @@ import pulseaudio_dlna.codecs import pulseaudio_dlna.rules import pulseaudio_dlna.plugins.renderer -import pyupnpv2 +from . import pyupnpv2 logger = logging.getLogger('pulseaudio_dlna.plugins.dlna.renderer') @@ -123,8 +123,7 @@ def play(self, url=None, codec=None, artist=None, title=None, thumb=None): pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException, pulseaudio_dlna.plugins.renderer.NoEncoderFoundException, - pulseaudio_dlna.plugins.renderer.NoSuitableHostFoundException)\ - as e: + pulseaudio_dlna.plugins.renderer.NoSuitableHostFoundException) as e: return 500, '"{}" : {}'.format(self.label, str(e)) except Exception: traceback.print_exc() diff --git a/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py b/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py index 5cef4942..d2985479 100644 --- a/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py +++ b/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import re @@ -23,7 +23,7 @@ def _get_header_map(header): header = re.findall(r"(?P.*?):(?P.*?)\n", header) header = { - k.strip().lower(): v.strip() for k, v in dict(header).items() + k.strip().lower(): v.strip() for k, v in list(dict(header).items()) } return header diff --git a/pulseaudio_dlna/plugins/dlna/ssdp/discover.py b/pulseaudio_dlna/plugins/dlna/ssdp/discover.py index 5a52694f..5c17510b 100644 --- a/pulseaudio_dlna/plugins/dlna/ssdp/discover.py +++ b/pulseaudio_dlna/plugins/dlna/ssdp/discover.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import socket import logging @@ -117,7 +117,7 @@ def _search(self, host, ssdp_ttl, ssdp_mx, ssdp_amount): def _send_discover(self, sock, ssdp_mx): msg = self.MSEARCH_MSG.format( - host=self.SSDP_ADDRESS, port=self.SSDP_PORT, mx=ssdp_mx) + host=self.SSDP_ADDRESS, port=self.SSDP_PORT, mx=ssdp_mx).encode() if self.USE_SINGLE_SOCKET: for addr in self.addresses: sock.setsockopt( diff --git a/pulseaudio_dlna/plugins/dlna/ssdp/listener.py b/pulseaudio_dlna/plugins/dlna/ssdp/listener.py index 7f1663c4..a9f81af5 100644 --- a/pulseaudio_dlna/plugins/dlna/ssdp/listener.py +++ b/pulseaudio_dlna/plugins/dlna/ssdp/listener.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,11 +15,11 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + from gi.repository import GObject -import SocketServer +import socketserver import logging import socket import struct @@ -32,7 +32,7 @@ logger = logging.getLogger('pulseaudio_dlna.plugins.dlna.ssdp') -class SSDPHandler(SocketServer.BaseRequestHandler): +class SSDPHandler(socketserver.BaseRequestHandler): SSDP_ALIVE = 'ssdp:alive' SSDP_BYEBYE = 'ssdp:byebye' @@ -70,7 +70,7 @@ def _get_method(self, method_header): return method_header.split(' ')[0] -class SSDPListener(SocketServer.UDPServer): +class SSDPListener(socketserver.UDPServer): SSDP_ADDRESS = '239.255.255.250' SSDP_PORT = 1900 @@ -89,7 +89,7 @@ def run(self, ttl=None): return self.allow_reuse_address = True - SocketServer.UDPServer.__init__( + socketserver.UDPServer.__init__( self, (self.host or '', self.SSDP_PORT), SSDPHandler) self.socket.setsockopt( socket.IPPROTO_IP, @@ -148,5 +148,5 @@ def shutdown(self, *args): class ThreadedSSDPListener( - GobjectMainLoopMixin, SocketServer.ThreadingMixIn, SSDPListener): + GobjectMainLoopMixin, socketserver.ThreadingMixIn, SSDPListener): pass diff --git a/pulseaudio_dlna/plugins/renderer.py b/pulseaudio_dlna/plugins/renderer.py index 3cb17cee..0da3d3c7 100644 --- a/pulseaudio_dlna/plugins/renderer.py +++ b/pulseaudio_dlna/plugins/renderer.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,12 +15,12 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import re import random -import urlparse -import urllib +import urllib.parse +import urllib.request, urllib.parse, urllib.error import functools import logging import base64 @@ -189,7 +189,7 @@ def codec(self): missing_encoders = [] for codec in self.codecs: - for identifier, encoder_type in codec.ENCODERS.items(): + for identifier, encoder_type in list(codec.ENCODERS.items()): encoder = encoder_type() if encoder.binary not in missing_encoders: missing_encoders.append(encoder.binary) @@ -257,7 +257,7 @@ def stop(self): raise NotImplementedError() def add_mime_type(self, mime_type): - for identifier, _type in pulseaudio_dlna.codecs.CODECS.iteritems(): + for identifier, _type in pulseaudio_dlna.codecs.CODECS.items(): if _type.accepts(mime_type): codec = _type(mime_type) if codec not in self.codecs: @@ -312,7 +312,7 @@ def set_rules_from_config(self, config): codec_type = pulseaudio_dlna.codecs.CODECS[ codec_properties['identifier']] codec = codec_type(codec_properties['mime_type']) - for k, v in codec_properties.iteritems(): + for k, v in codec_properties.items(): forbidden_attributes = ['mime_type', 'identifier', 'rules'] if hasattr(codec, k) and k not in forbidden_attributes: setattr(codec, k, v) @@ -338,12 +338,12 @@ def _encode_settings(self, settings, suffix=''): port=server_port, ) data_string = ','.join( - ['{}="{}"'.format(k, v) for k, v in settings.iteritems()]) + ['{}="{}"'.format(k, v) for k, v in settings.items()]) stream_name = '/{base_string}/{suffix}'.format( - base_string=urllib.quote(base64.b64encode(data_string)), + base_string=urllib.parse.quote(base64.b64encode(data_string.encode())), suffix=suffix, ) - return urlparse.urljoin(base_url, stream_name) + return urllib.parse.urljoin(base_url, stream_name) def get_stream_url(self): settings = { diff --git a/pulseaudio_dlna/pulseaudio.py b/pulseaudio_dlna/pulseaudio.py index 5f3752c0..7c049389 100644 --- a/pulseaudio_dlna/pulseaudio.py +++ b/pulseaudio_dlna/pulseaudio.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + from gi.repository import GObject @@ -227,7 +227,7 @@ def dbus_server_lookup(self): 'org.PulseAudio.ServerLookup1', 'Address', dbus_interface='org.freedesktop.DBus.Properties') - return unicode(address) + return str(address) except dbus.exceptions.DBusException: return None @@ -237,14 +237,14 @@ def get_modules(self): stdout=subprocess.PIPE) stdout, stderr = process.communicate() if process.returncode == 0: - matches = re.findall(r'(\d+)\s+([\w-]+)(.*?)\n', stdout) + matches = re.findall(r'(\d+)\s+([\w-]+)(.*?)\n', stdout.decode()) return [match[1] for match in matches] return None def load_module(self, module_name, options=None): command = ['pactl', 'load-module', module_name] if options: - for key, value in options.items(): + for key, value in list(options.items()): command.append('{}={}'.format(key, value)) process = subprocess.Popen(command, stdout=subprocess.PIPE) stdout, stderr = process.communicate() @@ -299,8 +299,8 @@ def new(self, bus, client_path): icon_bytes = properties.get('application.icon_name', []) binary_bytes = properties.get('application.process.binary', []) return PulseClient( - object_path=unicode(client_path), - index=unicode(obj.Get('org.PulseAudio.Core1.Client', 'Index')), + object_path=str(client_path), + index=str(obj.Get('org.PulseAudio.Core1.Client', 'Index')), name=self._convert_bytes_to_unicode(name_bytes), icon=self._convert_bytes_to_unicode(icon_bytes), binary=self._convert_bytes_to_unicode(binary_bytes), @@ -352,9 +352,9 @@ def new(self, bus, module_path): try: obj = bus.get_object(object_path=module_path) return PulseModule( - object_path=unicode(module_path), - index=unicode(obj.Get('org.PulseAudio.Core1.Module', 'Index')), - name=unicode(obj.Get('org.PulseAudio.Core1.Module', 'Name')), + object_path=str(module_path), + index=str(obj.Get('org.PulseAudio.Core1.Module', 'Index')), + name=str(obj.Get('org.PulseAudio.Core1.Module', 'Name')), ) except dbus.exceptions.DBusException: logger.error( @@ -400,13 +400,13 @@ def new(self, bus, object_path): properties = obj.Get('org.PulseAudio.Core1.Device', 'PropertyList') description_bytes = properties.get('device.description', []) - module_path = unicode( + module_path = str( obj.Get('org.PulseAudio.Core1.Device', 'OwnerModule')) return PulseSink( - object_path=unicode(object_path), - index=unicode(obj.Get('org.PulseAudio.Core1.Device', 'Index')), - name=unicode(obj.Get('org.PulseAudio.Core1.Device', 'Name')), + object_path=str(object_path), + index=str(obj.Get('org.PulseAudio.Core1.Device', 'Index')), + name=str(obj.Get('org.PulseAudio.Core1.Device', 'Name')), label=self._convert_bytes_to_unicode(description_bytes), module=PulseModuleFactory.new(bus, module_path), ) @@ -498,13 +498,13 @@ class PulseStreamFactory(object): def new(self, bus, stream_path): try: obj = bus.get_object(object_path=stream_path) - client_path = unicode( + client_path = str( obj.Get('org.PulseAudio.Core1.Stream', 'Client')) return PulseStream( - object_path=unicode(stream_path), - index=unicode(obj.Get( + object_path=str(stream_path), + index=str(obj.Get( 'org.PulseAudio.Core1.Stream', 'Index')), - device=unicode(obj.Get( + device=str(obj.Get( 'org.PulseAudio.Core1.Stream', 'Device')), client=PulseClientFactory.new(bus, client_path), ) diff --git a/pulseaudio_dlna/recorders.py b/pulseaudio_dlna/recorders.py index fb3270a2..1ac50cbe 100644 --- a/pulseaudio_dlna/recorders.py +++ b/pulseaudio_dlna/recorders.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import pulseaudio_dlna.codecs diff --git a/pulseaudio_dlna/rules.py b/pulseaudio_dlna/rules.py index 31690d87..67c9c984 100644 --- a/pulseaudio_dlna/rules.py +++ b/pulseaudio_dlna/rules.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import functools import logging @@ -44,7 +44,7 @@ def __eq__(self, other): if type(other) is type: return type(self) is other try: - if isinstance(other, basestring): + if isinstance(other, str): return type(self) is RULES[other] except: raise RuleNotFoundException(other) @@ -54,7 +54,7 @@ def __gt__(self, other): if type(other) is type: return type(self) > other try: - if isinstance(other, basestring): + if isinstance(other, str): return type(self) > RULES[other] except: raise RuleNotFoundException() @@ -63,7 +63,7 @@ def __gt__(self, other): def to_json(self): attributes = [] d = { - k: v for k, v in self.__dict__.iteritems() + k: v for k, v in iter(self.__dict__.items()) if k not in attributes } d['name'] = str(self) @@ -122,11 +122,11 @@ def append(self, *args): except KeyError: raise RuleNotFoundException(name) attributes = ['name'] - for k, v in arg.iteritems(): + for k, v in arg.items(): if hasattr(rule, k) and k not in attributes: setattr(rule, k, v) self._add_rule(rule) - elif isinstance(arg, basestring): + elif isinstance(arg, str): try: rule = RULES[arg]() self._add_rule(rule) diff --git a/pulseaudio_dlna/streamserver.py b/pulseaudio_dlna/streamserver.py index fd42ac66..422a46f0 100644 --- a/pulseaudio_dlna/streamserver.py +++ b/pulseaudio_dlna/streamserver.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + from gi.repository import GObject @@ -27,14 +27,14 @@ import select import sys import base64 -import urllib +import urllib.request, urllib.parse, urllib.error import json import os import signal import pkg_resources -import BaseHTTPServer -import SocketServer -import Queue +import http.server +import socketserver +import queue import threading import pulseaudio_dlna.encoders @@ -49,7 +49,7 @@ PROTOCOL_VERSION_V11 = 'HTTP/1.1' -class ProcessQueue(Queue.Queue): +class ProcessQueue(queue.Queue): def data(self): data = self.get() @@ -266,28 +266,28 @@ def __str__(self): '\n'.join( [' {}\n {}'.format( path, - ' '.join([str(s) for id, s in streams.items()])) - for path, streams in self.streams.items()], + ' '.join([str(s) for id, s in list(streams.items())])) + for path, streams in list(self.streams.items())], ), ) -class StreamRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): +class StreamRequestHandler(http.server.BaseHTTPRequestHandler): def __init__(self, *args): try: - BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args) + http.server.BaseHTTPRequestHandler.__init__(self, *args) except IOError: pass def do_HEAD(self): logger.debug('Got the following HEAD request:\n{header}'.format( - header=json.dumps(self.headers.items(), indent=2))) + header=json.dumps(list(self.headers.items()), indent=2))) item = self.get_requested_item() self.handle_headers(item) def do_GET(self): logger.debug('Got the following GET request:\n{header}'.format( - header=json.dumps(self.headers.items(), indent=2))) + header=json.dumps(list(self.headers.items()), indent=2))) item = self.get_requested_item() self.handle_headers(item) if isinstance(item, pulseaudio_dlna.images.BaseImage): @@ -344,7 +344,7 @@ def handle_headers(self, item): header=json.dumps(headers, indent=2), )) self.send_response(response_code) - for name, value in headers.items(): + for name, value in list(headers.items()): self.send_header(name, value) self.end_headers() @@ -385,7 +385,8 @@ def get_requested_item(self): def _decode_settings(self, path): try: data_quoted = re.findall(r'/(.*?)/', path)[0] - data_string = base64.b64decode(urllib.unquote(data_quoted)) + data_bytes = base64.b64decode(urllib.parse.unquote(data_quoted)) + data_string = data_bytes.decode() settings = { k: v for k, v in re.findall('(.*?)="(.*?)",?', data_string) } @@ -402,7 +403,7 @@ def log_message(self, format, *args): pass -class StreamServer(SocketServer.TCPServer): +class StreamServer(socketserver.TCPServer): HOST = None PORT = None @@ -423,7 +424,7 @@ def run(self): self.allow_reuse_address = True self.daemon_threads = True try: - SocketServer.TCPServer.__init__( + socketserver.TCPServer.__init__( self, (self.ip or '', self.port), StreamRequestHandler) except socket.error: logger.critical( @@ -486,5 +487,5 @@ def shutdown(self, *args): class ThreadedStreamServer( - GobjectMainLoopMixin, SocketServer.ThreadingMixIn, StreamServer): + GobjectMainLoopMixin, socketserver.ThreadingMixIn, StreamServer): pass diff --git a/pulseaudio_dlna/utils/encoding.py b/pulseaudio_dlna/utils/encoding.py index 86b14adb..84565951 100644 --- a/pulseaudio_dlna/utils/encoding.py +++ b/pulseaudio_dlna/utils/encoding.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging import sys @@ -34,10 +34,10 @@ def __init__(self, var): ) -def decode_default(bytes): - if type(bytes) is not str: - raise NotBytesException(bytes) - guess = chardet.detect(bytes) +def decode_default(in_bytes): + if type(in_bytes) is not bytes: + raise NotBytesException(in_bytes) + guess = chardet.detect(in_bytes) encodings = { 'sys.stdout.encoding': sys.stdout.encoding, 'locale.getpreferredencoding': locale.getpreferredencoding(), @@ -45,27 +45,27 @@ def decode_default(bytes): 'utf-8': 'utf-8', 'latin1': 'latin1', } - for encoding in encodings.values(): + for encoding in list(encodings.values()): if encoding and encoding != 'ascii': try: - return bytes.decode(encoding) + return in_bytes.decode(encoding) except UnicodeDecodeError: continue try: - return bytes.decode('ascii', errors='replace') + return in_bytes.decode('ascii', errors='replace') except UnicodeDecodeError: logger.error( 'Decoding failed using the following encodings: "{}"'.format( ','.join( - ['{}:{}'.format(f, e) for f, e in encodings.items()] + ['{}:{}'.format(f, e) for f, e in list(encodings.items())] ))) return 'Unknown' -def _bytes2hex(bytes, seperator=':'): - if type(bytes) is not str: - raise NotBytesException(bytes) - return seperator.join('{:02x}'.format(ord(b)) for b in bytes) +def _bytes2hex(in_bytes, seperator=':'): + if type(in_bytes) is not bytes: + raise NotBytesException(in_bytes) + return seperator.join('{:02x}'.format(ord(b)) for b in in_bytes) def _hex2bytes(hex, seperator=':'): diff --git a/pulseaudio_dlna/utils/git.py b/pulseaudio_dlna/utils/git.py index b2e911fe..20858540 100644 --- a/pulseaudio_dlna/utils/git.py +++ b/pulseaudio_dlna/utils/git.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import os diff --git a/pulseaudio_dlna/utils/network.py b/pulseaudio_dlna/utils/network.py index 47fbbee6..23473d77 100644 --- a/pulseaudio_dlna/utils/network.py +++ b/pulseaudio_dlna/utils/network.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import netifaces import traceback diff --git a/pulseaudio_dlna/utils/psutil.py b/pulseaudio_dlna/utils/psutil.py index 011e9766..793f4709 100644 --- a/pulseaudio_dlna/utils/psutil.py +++ b/pulseaudio_dlna/utils/psutil.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,8 +15,8 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import absolute_import -from __future__ import unicode_literals + + import logging import psutil diff --git a/pulseaudio_dlna/utils/subprocess.py b/pulseaudio_dlna/utils/subprocess.py index 20091632..48e0d00e 100644 --- a/pulseaudio_dlna/utils/subprocess.py +++ b/pulseaudio_dlna/utils/subprocess.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,8 +15,8 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import absolute_import -from __future__ import unicode_literals + + from gi.repository import GObject diff --git a/pulseaudio_dlna/workarounds.py b/pulseaudio_dlna/workarounds.py index c82b3bcd..9af928b0 100644 --- a/pulseaudio_dlna/workarounds.py +++ b/pulseaudio_dlna/workarounds.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -15,12 +15,12 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import logging from lxml import etree import requests -import urlparse +import urllib.parse import traceback @@ -170,7 +170,7 @@ def _parse_xml(self, xml): control_url = xml_root.find(self.MR_YAMAHA_CONTROLURL_PATH, namespaces) if ((url_base is None) or (control_url is None)): return False - ip, port = urlparse.urlparse(url_base.text).netloc.split(':') + ip, port = urllib.parse.urlparse(url_base.text).netloc.split(':') if ((not ip) or (not port)): return False diff --git a/scripts/chromecast-beam.py b/scripts/chromecast-beam.py index 5b594f37..a40d7163 100755 --- a/scripts/chromecast-beam.py +++ b/scripts/chromecast-beam.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # -*- coding: utf-8 -*- # This file is part of pulseaudio-dlna. @@ -110,7 +110,7 @@ ''' -from __future__ import unicode_literals + import docopt import logging @@ -125,8 +125,8 @@ import shutil import traceback import re -import SimpleHTTPServer -import SocketServer +import http.server +import socketserver import pulseaudio_dlna.utils.network import pulseaudio_dlna.plugins.chromecast.pycastv2 as pycastv2 @@ -193,7 +193,7 @@ def stop(host, port, timeout=5): stop(self.chromecast_host, self.PORT) -class ThreadedHTTPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): +class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): allow_reuse_address = True daemon_threads = True @@ -207,7 +207,7 @@ def __init__(self, file_uri, bind_host, request_host, port, handler=None): self.handler = handler or DefaultRequestHandler os.chdir(self.file_path) - SocketServer.TCPServer.__init__( + socketserver.TCPServer.__init__( self, (self.bind_host, self.port), self.handler) @property @@ -270,7 +270,7 @@ def _apply_option(cls, attribute, value): @classmethod def _apply_options(cls, options, option_map): - for option, value in cls._decode_settings(options).items(): + for option, value in list(cls._decode_settings(options).items()): attribute = option_map.get(option, None) cls._apply_option(attribute, value) @@ -381,7 +381,7 @@ def _transcode_cmd_str(cls): if cls.SUB_TITLES: options['soverlay'] = None return ','.join([ - '{}={}'.format(k, v) if v else k for k, v in options.items() + '{}={}'.format(k, v) if v else k for k, v in list(options.items()) ]) @classmethod @@ -410,18 +410,18 @@ def command(cls, file_path): ] -class DefaultRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): +class DefaultRequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self, *args, **kwargs): logger.info('Serving unmodified media file to {} ...'.format( self.client_address[0])) - SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self, *args, **kwargs) + http.server.SimpleHTTPRequestHandler.do_GET(self, *args, **kwargs) def log_request(self, code='-', size='-'): logger.info('{} - {}'.format(self.requestline, code)) -class TranscodeRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): +class TranscodeRequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): client_address = self.client_address[0] @@ -463,7 +463,7 @@ def get_external_ip(): # Local pulseaudio-dlna installations running in a virutalenv should run this # script as module: -# python -m scripts/chromecast-beam 192.168.1.10 ~/videos/test.mkv +# python3 -m scripts/chromecast-beam 192.168.1.10 ~/videos/test.mkv if __name__ == "__main__": diff --git a/scripts/fritzbox-device-sharing.py b/scripts/fritzbox-device-sharing.py index a6cd6cd5..f1de3bba 100755 --- a/scripts/fritzbox-device-sharing.py +++ b/scripts/fritzbox-device-sharing.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # -*- coding: utf-8 -*- # This file is part of pulseaudio-dlna. @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import requests import sys @@ -94,7 +94,7 @@ def discover_devices(self): def _discover_devices(self): holder = pulseaudio_dlna.holder.Holder(self.PLUGINS) holder.search(ttl=self.TIMEOUT) - return holder.devices.values() + return list(holder.devices.values()) ip_detector = IPDetector() @@ -113,5 +113,5 @@ def _discover_devices(self): for device in dlna_discover.devices: link = device.upnp_device.access_url.replace( device.ip, ip_detector.public_ip) - print(SHARE_PATTERN.format( - name=device.name, ip=device.ip, port=device.port, link=link)) + print((SHARE_PATTERN.format( + name=device.name, ip=device.ip, port=device.port, link=link))) diff --git a/scripts/radio.py b/scripts/radio.py index 2483e073..2f6d4b20 100755 --- a/scripts/radio.py +++ b/scripts/radio.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # -*- coding: utf-8 -*- # This file is part of pulseaudio-dlna. @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -from __future__ import unicode_literals + import requests import logging @@ -101,7 +101,7 @@ def _get_device(self, name, flavour=None): return None def _get_codec(self, url): - for identifier, _type in pulseaudio_dlna.codecs.CODECS.iteritems(): + for identifier, _type in pulseaudio_dlna.codecs.CODECS.items(): codec = _type() if url.endswith(codec.suffix): return codec @@ -118,14 +118,14 @@ def _discover_devices(self): holder = pulseaudio_dlna.holder.Holder(self.PLUGINS) holder.search(ttl=5) logger.info('Found the following devices:') - for udn, device in holder.devices.items(): + for udn, device in list(holder.devices.items()): logger.info(' - "{name}" ({flavour})'.format( name=device.name, flavour=device.flavour)) - return holder.devices.values() + return list(holder.devices.values()) # Local pulseaudio-dlna installations running in a virutalenv should run this # script as module: -# python -m scripts/radio [--list | --stop] +# python3 -m scripts/radio [--list | --stop] args = sys.argv[1:] rl = RadioLauncher() diff --git a/setup.py b/setup.py index 8edc44da..ffdff4bd 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # This file is part of pulseaudio-dlna. @@ -29,18 +29,18 @@ platforms="Debian GNU/Linux", classifiers=[ "Development Status :: 4 - Beta", - "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", "Environment :: Console", "Topic :: Multimedia :: Sound/Audio", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", ], - version='0.5.2', + version='0.6.0', py_modules=[], packages=setuptools.find_packages(), install_requires=[ "docopt >= 0.6.1", "requests >= 2.2.1", - "setproctitle >= 1.0.1", + "setproctitle >= 1.1.10", "protobuf >= 2.5.0", "notify2 >= 0.3", "psutil >= 1.2.1", From cc0c6833e43e059a22c80bf6f622128a41cdbfbf Mon Sep 17 00:00:00 2001 From: Andreas Harter Date: Thu, 18 Oct 2018 20:13:06 +0200 Subject: [PATCH 02/15] Upgrade dependencies to support python3 --- setup.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index ffdff4bd..10a6e998 100644 --- a/setup.py +++ b/setup.py @@ -41,13 +41,12 @@ "docopt >= 0.6.1", "requests >= 2.2.1", "setproctitle >= 1.1.10", - "protobuf >= 2.5.0", + "protobuf >= 3.0.0", "notify2 >= 0.3", - "psutil >= 1.2.1", - "futures >= 2.1.6", - "chardet >= 2.0.1", + "psutil >= 5.4.7", + "chardet >= 3.0.4", "pyroute2 >= 0.3.5", - "netifaces >= 0.8", + "netifaces >= 0.10.0", "lxml >= 3", "zeroconf >= 0.17.4", ], From dae2a6810be9504ffa90751f43b5af5921749e20 Mon Sep 17 00:00:00 2001 From: Andreas Harter Date: Thu, 18 Oct 2018 20:13:30 +0200 Subject: [PATCH 03/15] Add missing dependencies --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 10a6e998..a13c98c5 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,8 @@ "netifaces >= 0.10.0", "lxml >= 3", "zeroconf >= 0.17.4", + "PyGObject >= 3.3.0", + "dbus-python >= 1.0.0", ], entry_points={ "console_scripts": [ From 28e52b3d18694fad2ea2460b5375a3993c8185ab Mon Sep 17 00:00:00 2001 From: Andreas Harter Date: Thu, 18 Oct 2018 20:16:26 +0200 Subject: [PATCH 04/15] Upgrade Makefile and bootstrap.sh to python3 --- Makefile | 2 +- scripts/bootstrap.sh | 37 ++++++++++++++++++------------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 4bd3fc72..aecab9e8 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . -python ?= python2.7 +python ?= python3 user ?= $(shell whoami) all: pulseaudio_dlna.egg-info diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 487bf5b6..1c0596dd 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -59,31 +59,30 @@ function install_fonts() { function install_dev() { sudo apt-get install \ - python2.7 \ - python-pip \ - python-setuptools \ - python-dbus \ - python-docopt \ - python-requests \ - python-setproctitle \ - python-gi \ - python-protobuf \ - python-notify2 \ - python-psutil \ - python-concurrent.futures \ - python-chardet \ - python-netifaces \ - python-netaddr \ - python-pyroute2 \ - python-lxml \ - python-zeroconf \ + python3 \ + python3-pip \ + python3-setuptools \ + python3-dbus \ + python3-docopt \ + python3-requests \ + python3-setproctitle \ + python3-gi \ + python3-protobuf \ + python3-notify2 \ + python3-psutil \ + python3-chardet \ + python3-netifaces \ + python3-netaddr \ + python3-pyroute2 \ + python3-lxml \ + python3-zeroconf \ vorbis-tools \ sox \ lame \ flac \ opus-tools \ pavucontrol \ - virtualenv python-dev git-core + virtualenv python3-dev git-core [[ -d ~/pulseaudio-dlna ]] || \ git clone https://github.com/masmu/pulseaudio-dlna.git ~/pulseaudio-dlna } From d777f31344addd5e99c5f89e0b3f2b7c96c514c2 Mon Sep 17 00:00:00 2001 From: Andreas Harter Date: Sun, 21 Oct 2018 22:12:06 +0200 Subject: [PATCH 05/15] Re-enable chromecast support using pychromecast --- pulseaudio_dlna/application.py | 4 +- .../plugins/chromecast/__init__.py | 26 +- pulseaudio_dlna/plugins/chromecast/mdns.py | 82 ----- .../plugins/chromecast/pycastv2/__init__.py | 313 ----------------- .../chromecast/pycastv2/cast_channel_pb2.py | 321 ------------------ .../chromecast/pycastv2/cast_socket.py | 213 ------------ .../plugins/chromecast/pycastv2/commands.py | 194 ----------- .../plugins/chromecast/pycastv2/example.py | 36 -- .../plugins/chromecast/renderer.py | 134 ++------ scripts/bootstrap.sh | 3 +- scripts/chromecast-beam.py | 34 +- setup.py | 3 +- 12 files changed, 69 insertions(+), 1294 deletions(-) delete mode 100644 pulseaudio_dlna/plugins/chromecast/mdns.py delete mode 100644 pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py delete mode 100644 pulseaudio_dlna/plugins/chromecast/pycastv2/cast_channel_pb2.py delete mode 100644 pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py delete mode 100644 pulseaudio_dlna/plugins/chromecast/pycastv2/commands.py delete mode 100644 pulseaudio_dlna/plugins/chromecast/pycastv2/example.py diff --git a/pulseaudio_dlna/application.py b/pulseaudio_dlna/application.py index 50aac5f1..8f3d864a 100644 --- a/pulseaudio_dlna/application.py +++ b/pulseaudio_dlna/application.py @@ -32,7 +32,7 @@ import pulseaudio_dlna.plugins.dlna.ssdp import pulseaudio_dlna.plugins.dlna.ssdp.listener import pulseaudio_dlna.plugins.dlna.ssdp.discover -# import pulseaudio_dlna.plugins.chromecast +import pulseaudio_dlna.plugins.chromecast # import pulseaudio_dlna.plugins.chromecast.mdns import pulseaudio_dlna.encoders import pulseaudio_dlna.covermodes @@ -54,7 +54,7 @@ class Application(object): DEVICE_CONFIG = 'devices.json' PLUGINS = [ pulseaudio_dlna.plugins.dlna.DLNAPlugin(), - # pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), + pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), ] SHUTDOWN_TIMEOUT = 5 diff --git a/pulseaudio_dlna/plugins/chromecast/__init__.py b/pulseaudio_dlna/plugins/chromecast/__init__.py index 09bc5533..51f6e38a 100644 --- a/pulseaudio_dlna/plugins/chromecast/__init__.py +++ b/pulseaudio_dlna/plugins/chromecast/__init__.py @@ -18,9 +18,10 @@ import logging +import threading +import pychromecast import pulseaudio_dlna.plugins -import pulseaudio_dlna.plugins.chromecast.mdns from pulseaudio_dlna.plugins.chromecast.renderer import ChromecastRendererFactory logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast') @@ -28,8 +29,6 @@ class ChromecastPlugin(pulseaudio_dlna.plugins.BasePlugin): - GOOGLE_MDNS_DOMAIN = '_googlecast._tcp.local.' - def __init__(self, *args): pulseaudio_dlna.plugins.BasePlugin.__init__(self, *args) @@ -38,20 +37,17 @@ def lookup(self, url, xml): def discover(self, holder, ttl=None, host=None): self.holder = holder - mdns = pulseaudio_dlna.plugins.chromecast.mdns.MDNSListener( - domain=self.GOOGLE_MDNS_DOMAIN, - host=host, - cb_on_device_added=self._on_device_added, - cb_on_device_removed=self._on_device_removed - ) - mdns.run(ttl) + stop_discovery = pychromecast.get_chromecasts(blocking=False, callback=self._on_device_added) + + if ttl: + t = threading.Timer(ttl, stop_discovery) + t.start() + logger.info('ChromecastPlugin.discover()') @pulseaudio_dlna.plugins.BasePlugin.add_device_after - def _on_device_added(self, mdns_info): - if mdns_info: - return ChromecastRendererFactory.from_mdns_info(mdns_info) - return None + def _on_device_added(self, device): + return ChromecastRenderer(device, model_number, model_description, manufacturer) @pulseaudio_dlna.plugins.BasePlugin.remove_device_after - def _on_device_removed(self, mdns_info): + def _on_device_removed(self, device): return None diff --git a/pulseaudio_dlna/plugins/chromecast/mdns.py b/pulseaudio_dlna/plugins/chromecast/mdns.py deleted file mode 100644 index a7d56c43..00000000 --- a/pulseaudio_dlna/plugins/chromecast/mdns.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/python3 - -# This file is part of pulseaudio-dlna. - -# pulseaudio-dlna is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# pulseaudio-dlna is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with pulseaudio-dlna. If not, see . - - - -from gi.repository import GObject - -import logging -import zeroconf -import time - -logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast.mdns') - - -class MDNSHandler(object): - - def __init__(self, server): - self.server = server - - def add_service(self, zeroconf, type, name): - info = zeroconf.get_service_info(type, name) - if self.server.cb_on_device_added: - self.server.cb_on_device_added(info) - - def remove_service(self, zeroconf, type, name): - info = zeroconf.get_service_info(type, name) - if self.server.cb_on_device_removed: - self.server.cb_on_device_removed(info) - - -class MDNSListener(object): - - def __init__( - self, domain, - host=None, - cb_on_device_added=None, cb_on_device_removed=None): - self.domain = domain - self.host = host - self.cb_on_device_added = cb_on_device_added - self.cb_on_device_removed = cb_on_device_removed - - def run(self, ttl=None): - if self.host: - self.zeroconf = zeroconf.Zeroconf(interfaces=[self.host]) - else: - self.zeroconf = zeroconf.Zeroconf() - zeroconf.ServiceBrowser(self.zeroconf, self.domain, MDNSHandler(self)) - - if ttl: - GObject.timeout_add(ttl * 1000, self.shutdown) - - self.__running = True - self.__mainloop = GObject.MainLoop() - context = self.__mainloop.get_context() - try: - while self.__running: - if context.pending(): - context.iteration(True) - else: - time.sleep(0.01) - except KeyboardInterrupt: - pass - self.zeroconf.close() - logger.info('MDNSListener.run()') - - def shutdown(self): - logger.info('MDNSListener.shutdown()') - self.__running = False diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py deleted file mode 100644 index 5d3f4357..00000000 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/python3 - -# This file is part of pulseaudio-dlna. - -# pulseaudio-dlna is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# pulseaudio-dlna is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with pulseaudio-dlna. If not, see . - - - -import time -import logging - -from . import commands -from . import cast_socket - -logger = logging.getLogger('pycastv2') - - -class ChannelClosedException(Exception): - pass - - -class TimeoutException(Exception): - pass - - -class LaunchErrorException(Exception): - pass - - -class ChannelController(object): - def __init__(self, socket): - self.request_id = 1 - self.transport_id = 'receiver-0' - self.session_id = None - self.app_id = None - - self.channels = [] - - self.socket = socket - self.socket.add_send_listener(self._handle_send) - self.socket.add_read_listener(self._handle_response) - self.socket.send_and_wait(commands.StatusCommand()) - - def _get_unused_request_id(self): - self.request_id += 1 - return self.request_id - 1 - - def _handle_send(self, command): - if command.request_id is not None: - command.request_id = (command.request_id or - self._get_unused_request_id()) - if command.session_id is not None: - command.session_id = command.session_id or self.session_id - command.sender_id = command.sender_id or 'sender-0' - if command.destination_id is None: - command.destination_id = 'receiver-0' - else: - command.destination_id = (command.destination_id or - self.transport_id) - if not self.is_channel_connected(command.destination_id): - self.connect_channel(command.destination_id) - return command - - def _handle_response(self, response): - if 'type' in response: - response_type = response['type'] - if response_type == 'RECEIVER_STATUS': - if 'applications' in response['status']: - applications = response['status']['applications'][0] - self.transport_id = ( - applications.get('transportId') or self.transport_id) - self.session_id = ( - applications.get('sessionId') or self.session_id) - self.app_id = ( - applications.get('appId') or self.app_id) - else: - self.transport_id = 'receiver-0' - self.session_id = None - self.app_id = None - elif response_type == 'PING': - self.socket.send(commands.PongCommand()) - elif response_type == 'CLOSE': - raise ChannelClosedException() - elif response_type == 'LAUNCH_ERROR': - raise LaunchErrorException() - - def is_channel_connected(self, destination_id): - return destination_id in self.channels - - def connect_channel(self, destination_id): - self.channels.append(destination_id) - self.socket.send(commands.ConnectCommand(destination_id)) - - def disconnect_channel(self, destination_id): - self.socket.send(commands.CloseCommand(destination_id)) - self.channels.remove(destination_id) - - def __str__(self): - return ('\n' - ' request_id: {request_id}\n' - ' transport_id: {transport_id}\n' - ' session_id: {session_id}\n' - ' app_id: {app_id}'.format( - request_id=self.request_id, - transport_id=self.transport_id, - session_id=self.session_id, - app_id=self.app_id)) - - -class ChromecastController(): - - APP_BACKDROP = 'E8C28D3C' - WAIT_INTERVAL = 0.1 - - def __init__(self, ip, port, timeout=10): - self.timeout = timeout - self.socket = cast_socket.CastSocket(ip, port) - self.channel_controller = ChannelController(self.socket) - - def is_app_running(self, app_id): - return self.channel_controller.app_id == app_id - - def launch_application(self, app_id): - if not self.is_app_running(app_id): - self.socket.send(commands.LaunchCommand(app_id)) - start_time = time.time() - while not self.is_app_running(app_id): - self.socket.send_and_wait(commands.StatusCommand()) - current_time = time.time() - if current_time - start_time > self.timeout: - raise TimeoutException() - time.sleep(self.WAIT_INTERVAL) - else: - logger.debug('Starting not necessary. Application is running ...') - - def stop_application(self): - if not self.is_app_running(self.APP_BACKDROP): - self.socket.send(commands.StopCommand()) - start_time = time.time() - while not self.is_app_running(None): - self.socket.send_and_wait(commands.StatusCommand()) - current_time = time.time() - if current_time - start_time > self.timeout: - raise TimeoutException() - time.sleep(self.WAIT_INTERVAL) - else: - logger.debug('Stop not necessary. Backdrop is running ...') - - def disconnect_application(self): - if not self.is_app_running(self.APP_BACKDROP): - self.socket.send(commands.CloseCommand(destination_id=False)) - start_time = time.time() - while not self.is_app_running(None): - try: - self.socket.send_and_wait(commands.StatusCommand()) - except cast_socket.ConnectionTerminatedException: - break - current_time = time.time() - if current_time - start_time > self.timeout: - raise TimeoutException() - time.sleep(self.WAIT_INTERVAL) - else: - logger.debug('Closing not necessary. Backdrop is running ...') - - def wait(self, timeout): - self.socket.wait(timeout) - - def cleanup(self): - self.socket.close() - - -class LoadCommand(commands.BaseCommand): - def __init__(self, url, mime_type, artist=None, title=None, thumb=None, - session_id=None, destination_id=None, namespace=None): - commands.BaseCommand.__init__(self) - self.data = { - 'autoplay': True, - 'currentTime': 0, - 'media': {'contentId': url, - 'contentType': mime_type, - 'streamType': 'LIVE', - }, - 'type': 'LOAD' - } - if artist or title or thumb: - self.data['media']['metadata'] = { - 'metadataType': 3, - } - if artist: - self.data['media']['metadata']['artist'] = artist - if title: - self.data['media']['metadata']['title'] = title - if thumb: - self.data['media']['metadata']['images'] = [ - {'url': thumb}, - ] - - self.request_id = False - self.session_id = False - self.destination_id = destination_id - self.namespace = namespace or 'urn:x-cast:com.google.cast.media' - - -class LoadFailedException(Exception): - pass - - -class MediaPlayerController(ChromecastController): - - APP_MEDIA_PLAYER = 'CC1AD845' - - PLAYER_STATE_BUFFERING = 'BUFFERING' - PLAYER_STATE_PLAYING = 'PLAYING' - PLAYER_STATE_PAUSED = 'PAUSED' - PLAYER_STATE_IDLE = 'IDLE' - - def __init__(self, ip, port, timeout=10): - ChromecastController.__init__(self, ip, port, timeout) - self._media_session_id = None - self._current_time = None - self._media = None - self._playback_rate = None - self._volume = None - self._player_state = None - - self.socket.add_read_listener(self._handle_response) - - def launch(self): - self.launch_application(self.APP_MEDIA_PLAYER) - - def load(self, url, mime_type, artist=None, title=None, thumb=None): - self.launch() - try: - self.socket.send_and_wait( - LoadCommand( - url, mime_type, - artist=artist, - title=title, - thumb=thumb, - destination_id=False)) - return True - except (cast_socket.NoResponseException, LoadFailedException): - return False - - def set_volume(self, volume): - self.socket.send_and_wait(commands.SetVolumeCommand(volume)) - - def set_mute(self, muted): - self.socket.send_and_wait(commands.SetVolumeMuteCommand(muted)) - - def _update_attribute(self, name, value): - if value is not None: - setattr(self, name, value) - - def _handle_response(self, response): - if 'type' in response: - if response['type'] == 'MEDIA_STATUS': - try: - status = response['status'][0] - except IndexError: - return - self._update_attribute( - '_media_session_id', status.get('mediaSessionId', None)) - self._update_attribute( - '_current_time', status.get('currentTime', None)) - self._update_attribute( - '_media', status.get('media', None)) - self._update_attribute( - '_playback_rate', status.get('playbackRate', None)) - self._update_attribute( - '_volume', status.get('volume', None)) - self._update_attribute( - '_player_state', status.get('playerState', None)) - elif response['type'] == 'LOAD_FAILED': - raise LoadFailedException() - - @property - def player_state(self): - return self._player_state - - @property - def is_playing(self): - return (self._player_state is not None and - self._player_state == self.PLAYER_STATE_PLAYING) - - @property - def is_paused(self): - return (self._player_state is not None and - self._player_state == self.PLAYER_STATE_PAUSED) - - @property - def is_idle(self): - return (self._player_state is not None and - self._player_state == self.PLAYER_STATE_IDLE) - - @property - def volume(self): - return self._volume.get('level') if self._volume else None - - @property - def is_muted(self): - return self._volume.get('muted') if self._volume else None diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_channel_pb2.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_channel_pb2.py deleted file mode 100644 index ee69a102..00000000 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_channel_pb2.py +++ /dev/null @@ -1,321 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: cast_channel.proto - -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 descriptor_pb2 -# @@protoc_insertion_point(imports) - - - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='cast_channel.proto', - package='extensions.api.cast_channel', - serialized_pb='\n\x12\x63\x61st_channel.proto\x12\x1b\x65xtensions.api.cast_channel\"\xe3\x02\n\x0b\x43\x61stMessage\x12R\n\x10protocol_version\x18\x01 \x02(\x0e\x32\x38.extensions.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\x12J\n\x0cpayload_type\x18\x05 \x02(\x0e\x32\x34.extensions.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\"\x0f\n\rAuthChallenge\"B\n\x0c\x41uthResponse\x12\x11\n\tsignature\x18\x01 \x02(\x0c\x12\x1f\n\x17\x63lient_auth_certificate\x18\x02 \x02(\x0c\"~\n\tAuthError\x12\x44\n\nerror_type\x18\x01 \x02(\x0e\x32\x30.extensions.api.cast_channel.AuthError.ErrorType\"+\n\tErrorType\x12\x12\n\x0eINTERNAL_ERROR\x10\x00\x12\n\n\x06NO_TLS\x10\x01\"\xc6\x01\n\x11\x44\x65viceAuthMessage\x12=\n\tchallenge\x18\x01 \x01(\x0b\x32*.extensions.api.cast_channel.AuthChallenge\x12;\n\x08response\x18\x02 \x01(\x0b\x32).extensions.api.cast_channel.AuthResponse\x12\x35\n\x05\x65rror\x18\x03 \x01(\x0b\x32&.extensions.api.cast_channel.AuthErrorB\x02H\x03') - - - -_CASTMESSAGE_PROTOCOLVERSION = _descriptor.EnumDescriptor( - name='ProtocolVersion', - full_name='extensions.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=335, - serialized_end=368, -) - -_CASTMESSAGE_PAYLOADTYPE = _descriptor.EnumDescriptor( - name='PayloadType', - full_name='extensions.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=370, - serialized_end=407, -) - -_AUTHERROR_ERRORTYPE = _descriptor.EnumDescriptor( - name='ErrorType', - full_name='extensions.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), - ], - containing_type=None, - options=None, - serialized_start=577, - serialized_end=620, -) - - -_CASTMESSAGE = _descriptor.Descriptor( - name='CastMessage', - full_name='extensions.api.cast_channel.CastMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='protocol_version', full_name='extensions.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.api.cast_channel.CastMessage.source_id', index=1, - number=2, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=bytes("", "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.api.cast_channel.CastMessage.destination_id', index=2, - number=3, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=bytes("", "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.api.cast_channel.CastMessage.namespace', index=3, - number=4, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=bytes("", "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.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.api.cast_channel.CastMessage.payload_utf8', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=bytes("", "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.api.cast_channel.CastMessage.payload_binary', index=6, - number=7, type=12, cpp_type=9, label=1, - 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=[], - enum_types=[ - _CASTMESSAGE_PROTOCOLVERSION, - _CASTMESSAGE_PAYLOADTYPE, - ], - options=None, - is_extendable=False, - extension_ranges=[], - serialized_start=52, - serialized_end=407, -) - - -_AUTHCHALLENGE = _descriptor.Descriptor( - name='AuthChallenge', - full_name='extensions.api.cast_channel.AuthChallenge', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - serialized_start=409, - serialized_end=424, -) - - -_AUTHRESPONSE = _descriptor.Descriptor( - name='AuthResponse', - full_name='extensions.api.cast_channel.AuthResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='signature', full_name='extensions.api.cast_channel.AuthResponse.signature', index=0, - number=1, type=12, cpp_type=9, label=2, - 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='client_auth_certificate', full_name='extensions.api.cast_channel.AuthResponse.client_auth_certificate', index=1, - number=2, type=12, cpp_type=9, label=2, - 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=[], - enum_types=[ - ], - options=None, - is_extendable=False, - extension_ranges=[], - serialized_start=426, - serialized_end=492, -) - - -_AUTHERROR = _descriptor.Descriptor( - name='AuthError', - full_name='extensions.api.cast_channel.AuthError', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='error_type', full_name='extensions.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, - extension_ranges=[], - serialized_start=494, - serialized_end=620, -) - - -_DEVICEAUTHMESSAGE = _descriptor.Descriptor( - name='DeviceAuthMessage', - full_name='extensions.api.cast_channel.DeviceAuthMessage', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='challenge', full_name='extensions.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.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.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, - extension_ranges=[], - serialized_start=623, - serialized_end=821, -) - -_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; -_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 - -class CastMessage(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): - DESCRIPTOR = _CASTMESSAGE - - # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.CastMessage) - -class AuthChallenge(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): - DESCRIPTOR = _AUTHCHALLENGE - - # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthChallenge) - -class AuthResponse(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): - DESCRIPTOR = _AUTHRESPONSE - - # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthResponse) - -class AuthError(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): - DESCRIPTOR = _AUTHERROR - - # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.AuthError) - -class DeviceAuthMessage(_message.Message, metaclass=_reflection.GeneratedProtocolMessageType): - DESCRIPTOR = _DEVICEAUTHMESSAGE - - # @@protoc_insertion_point(class_scope:extensions.api.cast_channel.DeviceAuthMessage) - - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), 'H\003') -# @@protoc_insertion_point(module_scope) diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py deleted file mode 100644 index 04e8ef52..00000000 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/python3 - -# This file is part of pulseaudio-dlna. - -# pulseaudio-dlna is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# pulseaudio-dlna is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with pulseaudio-dlna. If not, see . - - - -import ssl -import socket -import logging -import struct -import json -import time -import traceback -import select - -from . import cast_channel_pb2 - -logger = logging.getLogger('pycastv2.cast_socket') - - -class NoResponseException(Exception): - pass - - -class ConnectionTerminatedException(Exception): - pass - - -class BaseChromecastSocket(object): - def __init__(self, ip, port): - self.sock = socket.socket() - self.sock = ssl.wrap_socket(self.sock) - self.sock.connect((ip, port)) - self.agent = 'chromecast_v2' - - def _generate_message(self, - source_id='sender-0', destination_id='receiver-0', - namespace=None): - message = cast_channel_pb2.CastMessage() - message.protocol_version = message.CASTV2_1_0 - message.source_id = source_id - message.destination_id = destination_id - message.payload_type = cast_channel_pb2.CastMessage.STRING - if namespace: - message.namespace = namespace - return message - - def close(self): - self.sock.close() - logger.debug('Chromecast socket was cleaned up.') - - def send(self, data, sender_id, destination_id, namespace=None): - json_data = json.dumps(data) - message = self._generate_message( - source_id=sender_id, - destination_id=destination_id, - namespace=namespace) - message.payload_utf8 = json_data - size = struct.pack('>I', message.ByteSize()) - formatted_message = size + message.SerializeToString() - self.sock.sendall(formatted_message) - - def read(self, timeout=10): - try: - start_time = time.time() - data = str('') - while len(data) < 4: - if time.time() - start_time > timeout: - raise NoResponseException() - part = self.sock.recv(1) - if len(part) == 0: - raise ConnectionTerminatedException() - data += part - length = struct.unpack('>I', data)[0] - data = str('') - while len(data) < length: - part = self.sock.recv(2048) - data += part - message = self._generate_message() - message.ParseFromString(data) - response = json.loads(message.payload_utf8) - return response - except ssl.SSLError as e: - if e.message == 'The read operation timed out': - raise NoResponseException() - else: - logger.debug('Catched exception:') - traceback.print_exc() - return {} - - -class CastSocket(BaseChromecastSocket): - def __init__(self, ip, port): - BaseChromecastSocket.__init__(self, ip, port) - self.read_listeners = [] - self.send_listeners = [] - self.response_cache = {} - - def send(self, command): - for listener in self.send_listeners: - command = listener(command) - logger.debug('Sending message:\n{command}'.format( - command=command)) - BaseChromecastSocket.send( - self, - data=command.data, - sender_id=command.sender_id, - destination_id=command.destination_id, - namespace=command.namespace) - return command.request_id - - def read(self, timeout=None): - if timeout is not None: - self.wait_for_read(timeout) - response = BaseChromecastSocket.read(self) - logger.debug('Recieved message:\n {message}'.format( - message=json.dumps(response, indent=2))) - for listener in self.read_listeners: - listener(response) - return response - - def add_read_listener(self, listener): - self.read_listeners.append(listener) - - def add_send_listener(self, listener): - self.send_listeners.append(listener) - - def send_and_wait(self, command): - req_id = self.send(command) - return self.wait_for_response_id(req_id) - - def wait_for_read(self, timeout=None): - start_time = time.time() - while True: - if self._is_socket_readable(): - return - current_time = time.time() - if current_time - start_time > timeout: - raise NoResponseException() - time.sleep(0.1) - - def wait_for_response_id(self, req_id, timeout=10): - start_time = time.time() - while True: - if not self._is_socket_readable(): - time.sleep(0.1) - else: - response = self.read() - self._add_to_response_cache(response) - if req_id in self.response_cache: - return response - current_time = time.time() - if current_time - start_time > timeout: - raise NoResponseException() - return None - - def wait_for_response_type(self, _type, timeout=10): - start_time = time.time() - while True: - if not self._is_socket_readable(): - time.sleep(0.1) - else: - response = self.read() - self._add_to_response_cache(response) - if response.get('type', None) == _type: - return response - current_time = time.time() - if current_time - start_time > timeout: - raise NoResponseException() - return None - - def wait(self, timeout=10): - start_time = time.time() - while True: - if not self._is_socket_readable(): - time.sleep(0.1) - else: - response = self.read() - self._add_to_response_cache(response) - current_time = time.time() - if current_time - start_time > timeout: - return - return None - - def _add_to_response_cache(self, response): - if 'requestId' in response: - req_id = response['requestId'] - if int(req_id) != 0: - self.response_cache[req_id] = response - - def _is_socket_readable(self): - try: - r, w, e = select.select([self.sock], [], [self.sock], 0) - for sock in r: - return True - for sock in e: - raise NoResponseException() - except socket.error: - raise NoResponseException() - return False diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/commands.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/commands.py deleted file mode 100644 index efb4dd95..00000000 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/commands.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/python3 - -# This file is part of pulseaudio-dlna. - -# pulseaudio-dlna is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# pulseaudio-dlna is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with pulseaudio-dlna. If not, see . - - - - -class BaseCommand(object): - def __init__(self): - self._sender_id = None - self._destination_id = None - self._namespace = None - self._data = None - - @property - def sender_id(self): - return self._sender_id - - @sender_id.setter - def sender_id(self, value): - self._sender_id = value - - @property - def destination_id(self): - return self._destination_id - - @destination_id.setter - def destination_id(self, value): - self._destination_id = value - - @property - def request_id(self): - if 'requestId' in self.data: - return self.data['requestId'] - return 0 - - @request_id.setter - def request_id(self, value): - if value is not None: - self.data['requestId'] = value - - @property - def session_id(self): - if 'sessionId' in self.data: - return self.data['sessionId'] - return None - - @session_id.setter - def session_id(self, value): - if value is not None: - self.data['sessionId'] = value - - @property - def namespace(self): - return self._namespace - - @namespace.setter - def namespace(self, value): - self._namespace = value - - @property - def data(self): - return self._data - - @data.setter - def data(self, value): - self._data = value - - def __str__(self): - return ('<{class_name}>\n' - ' namespace: {namespace}\n' - ' destination_id: {destination_id}\n' - ' data: {data}'.format( - class_name=self.__class__.__name__, - namespace=self.namespace, - destination_id=self.destination_id, - data=self.data)) - - -class ConnectCommand(BaseCommand): - def __init__(self, destination_id=None, namespace=None, agent=None): - BaseCommand.__init__(self) - self.data = { - 'origin': {}, - 'type': 'CONNECT', - 'userAgent': agent or 'Unknown' - } - self.destination_id = destination_id - self.namespace = (namespace or - 'urn:x-cast:com.google.cast.tp.connection') - - -class CloseCommand(BaseCommand): - def __init__(self, destination_id=None, namespace=None): - BaseCommand.__init__(self) - self.data = { - 'origin': {}, - 'type': 'CLOSE' - } - self.destination_id = destination_id - self.namespace = (namespace or - 'urn:x-cast:com.google.cast.tp.connection') - - -class StatusCommand(BaseCommand): - def __init__(self, destination_id=None, namespace=None): - BaseCommand.__init__(self) - self.data = { - 'type': 'GET_STATUS' - } - self.request_id = False - self.destination_id = destination_id - self.namespace = namespace or 'urn:x-cast:com.google.cast.receiver' - - -class LaunchCommand(BaseCommand): - def __init__(self, app_id, destination_id=None, namespace=None): - BaseCommand.__init__(self) - self.data = { - 'appId': app_id, - 'type': 'LAUNCH' - } - self.request_id = False - self.destination_id = destination_id - self.namespace = namespace or 'urn:x-cast:com.google.cast.receiver' - - -class StopCommand(BaseCommand): - def __init__(self, session_id=False, destination_id=None, - namespace=None): - BaseCommand.__init__(self) - self.data = { - 'type': 'STOP' - } - self.session_id = False - self.request_id = False - self.destination_id = destination_id - self.namespace = namespace or 'urn:x-cast:com.google.cast.receiver' - - -class SetVolumeCommand(BaseCommand): - def __init__(self, volume, session_id=False, destination_id=None, - namespace=None): - BaseCommand.__init__(self) - self.data = { - 'type': 'SET_VOLUME', - 'volume': { - 'level': volume, - }, - } - self.request_id = False - self.destination_id = destination_id - self.namespace = namespace or 'urn:x-cast:com.google.cast.receiver' - - -class SetVolumeMuteCommand(BaseCommand): - def __init__(self, muted, session_id=False, destination_id=None, - namespace=None): - BaseCommand.__init__(self) - self.data = { - 'type': 'SET_VOLUME', - 'volume': { - 'muted': muted, - }, - } - self.request_id = False - self.destination_id = destination_id - self.namespace = namespace or 'urn:x-cast:com.google.cast.receiver' - - -class PongCommand(BaseCommand): - def __init__(self, session_id=False, destination_id=None, - namespace=None): - BaseCommand.__init__(self) - self.data = { - 'type': 'PONG' - } - self.session_id = False - self.request_id = False - self.destination_id = destination_id - self.namespace = namespace or 'urn:x-cast:com.google.cast.tp.heartbeat' diff --git a/pulseaudio_dlna/plugins/chromecast/pycastv2/example.py b/pulseaudio_dlna/plugins/chromecast/pycastv2/example.py deleted file mode 100644 index a8caaff4..00000000 --- a/pulseaudio_dlna/plugins/chromecast/pycastv2/example.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python3 - -# This file is part of pulseaudio-dlna. - -# pulseaudio-dlna is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# pulseaudio-dlna is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with pulseaudio-dlna. If not, see . - - - -import logging -from . import __init__ as pycastv2 - -logging.basicConfig(level=logging.DEBUG) - -mc = pycastv2.MediaPlayerController('192.168.1.3') -try: - mc.load('http://192.168.1.2:8080/stream.mp3', 'audio/mpeg') - mc.wait(10) - mc.disconnect_application() - # mc.stop_application() -except pycastv2.ChannelClosedException: - print('Channel was closed.') -except pycastv2.TimeoutException: - print('Request timed out.') -finally: - mc.cleanup() diff --git a/pulseaudio_dlna/plugins/chromecast/renderer.py b/pulseaudio_dlna/plugins/chromecast/renderer.py index e796b511..6b5d4e80 100644 --- a/pulseaudio_dlna/plugins/chromecast/renderer.py +++ b/pulseaudio_dlna/plugins/chromecast/renderer.py @@ -24,31 +24,36 @@ import traceback import lxml -from . import pycastv2 import pulseaudio_dlna.plugins.renderer import pulseaudio_dlna.rules import pulseaudio_dlna.codecs +from pychromecast import Chromecast +from pychromecast.dial import DeviceStatus, CAST_TYPES, CAST_TYPE_CHROMECAST +from pychromecast.error import PyChromecastError + logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast.renderer') class ChromecastRenderer(pulseaudio_dlna.plugins.renderer.BaseRenderer): - def __init__( - self, name, ip, port, udn, model_name, model_number, - model_description, manufacturer): + def __init__(self, + chromecast, + model_number=None, + model_description=None): pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__( self, - udn=udn, + udn=chromecast.uuid, flavour='Chromecast', - name=name, - ip=ip, - port=port or 8009, - model_name=model_name, + name=chromecast.name, + ip=chromecast.host, + port=chromecast.port, + model_name=chromecast.model_name, model_number=model_number, model_description=model_description, - manufacturer=manufacturer + manufacturer=chromecast.device.manufacturer ) + self.chromecast = chromecast def activate(self, config): if config: @@ -68,38 +73,12 @@ def play(self, url=None, codec=None, artist=None, title=None, thumb=None): self._before_play() url = url or self.get_stream_url() try: - cast = pycastv2.MediaPlayerController( - self.ip, self.port, self.REQUEST_TIMEOUT) - cast.load( - url, - mime_type=self.codec.mime_type, - artist=artist, - title=title, - thumb=thumb) + # TODO: artist missing + self.chromecast.play_media(url, mime_type=self.codec.mime_type, title=title, thumb=thumb) self.state = self.STATE_PLAYING return 200, None - except pycastv2.LaunchErrorException: - message = 'The media player could not be launched. ' \ - 'Maybe the chromecast is still closing a ' \ - 'running player instance. Try again in 30 seconds.' - return 503, message - except pycastv2.ChannelClosedException: - message = 'Connection was closed. I guess another ' \ - 'client is attached to it.' - return 423, message - except pycastv2.TimeoutException: - message = 'PLAY command - Could no connect to "{device}". ' \ - 'Connection timeout.'.format(device=self.label) - return 408, message - except socket.error as e: - if e.errno == 111: - message = 'The chromecast refused the connection. ' \ - 'Perhaps it does not support the castv2 ' \ - 'protocol.' - return 403, message - else: - traceback.print_exc() - return 500, None + except PyChromecastError as e: + return 500, str(e) except (pulseaudio_dlna.plugins.renderer.NoEncoderFoundException, pulseaudio_dlna.plugins.renderer.NoSuitableHostFoundException) as e: return 500, e @@ -108,39 +87,21 @@ def play(self, url=None, codec=None, artist=None, title=None, thumb=None): return 500, 'Unknown exception.' finally: self._after_play() - cast.cleanup() def stop(self): self._before_stop() try: - cast = pycastv2.MediaPlayerController( - self.ip, self.port, self.REQUEST_TIMEOUT) self.state = self.STATE_STOPPED - cast.disconnect_application() + self.chromecast.stop() + self.chromecast.disconnect() return 200, None - except pycastv2.ChannelClosedException: - message = 'Connection was closed. I guess another ' \ - 'client is attached to it.' - return 423, message - except pycastv2.TimeoutException: - message = 'STOP command - Could no connect to "{device}". ' \ - 'Connection timeout.'.format(device=self.label) - return 408, message - except socket.error as e: - if e.errno == 111: - message = 'The chromecast refused the connection. ' \ - 'Perhaps it does not support the castv2 ' \ - 'protocol.' - return 403, message - else: - traceback.print_exc() - return 500, 'Unknown exception.' + except PyChromecastError as e: + return 500, e except Exception: traceback.print_exc() return 500, 'Unknown exception.' finally: self._after_stop() - cast.cleanup() def pause(self): raise NotImplementedError() @@ -200,7 +161,7 @@ def from_xml(cls, url, xml): device_modelname.text)) return None - return ChromecastRenderer( + return cls.from_properties( name=str(device_friendlyname.text), ip=str(ip), port=None, @@ -220,37 +181,16 @@ def from_header(cls, header): return cls.from_url(header['location']) @classmethod - def from_mdns_info(cls, info): - - def _bytes2string(bytes): - ip = [] - for b in bytes: - subnet = int(b.encode('hex'), 16) - ip.append(str(subnet)) - return '.'.join(ip) - - def _get_device_info(info): - try: - return { - 'udn': '{}:{}'.format('uuid', info.properties['id']), - 'type': info.properties['md'].decode('utf-8'), - 'name': info.properties['fn'].decode('utf-8'), - 'ip': _bytes2string(info.address), - 'port': int(info.port), - } - except (KeyError, AttributeError, TypeError): - return None - - device_info = _get_device_info(info) - if device_info: - return ChromecastRenderer( - name=device_info['name'], - ip=device_info['ip'], - port=device_info['port'], - udn=device_info['udn'], - model_name=device_info['type'], - model_number=None, - model_description=None, - manufacturer='Google Inc.' - ) - return None + def from_properties( + cls, name, ip, port, udn, model_name, model_number, + model_description, manufacturer): + cast_type = CAST_TYPES.get(model_name.lower(), + CAST_TYPE_CHROMECAST) + device = DeviceStatus( + friendly_name=name, model_name=model_name, + manufacturer=manufacturer, uuid=udn, cast_type=cast_type, + ) + chromecast = Chromecast(host=ip, port=port or 8009, device=device) + return ChromecastRenderer(chromecast=chromecast, + model_number=model_number, + model_description=model_description) diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 1c0596dd..4d4b0a35 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -67,7 +67,6 @@ function install_dev() { python3-requests \ python3-setproctitle \ python3-gi \ - python3-protobuf \ python3-notify2 \ python3-psutil \ python3-chardet \ @@ -75,7 +74,7 @@ function install_dev() { python3-netaddr \ python3-pyroute2 \ python3-lxml \ - python3-zeroconf \ + python3-pychromecast \ vorbis-tools \ sox \ lame \ diff --git a/scripts/chromecast-beam.py b/scripts/chromecast-beam.py index a40d7163..45602a58 100755 --- a/scripts/chromecast-beam.py +++ b/scripts/chromecast-beam.py @@ -129,7 +129,7 @@ import socketserver import pulseaudio_dlna.utils.network -import pulseaudio_dlna.plugins.chromecast.pycastv2 as pycastv2 +from pychromecast import Chromecast logger = logging.getLogger('chromecast-beam') @@ -162,35 +162,35 @@ class ChromecastThread(StoppableThread): def __init__(self, chromecast_host, media_url, mime_type=None, *args, **kwargs): StoppableThread.__init__(self, *args, **kwargs) - self.chromecast_host = chromecast_host self.media_url = media_url self.mime_type = mime_type or 'video/mp4' self.desired_volume = 1.0 + self.chromecast = Chromecast(host=chromecast_host, port=PORT, timeout=5) def run(self): - def play(host, port, url, mime_type, timeout=5): - cast = pycastv2.MediaPlayerController(host, port, timeout) - cast.load(url, mime_type=mime_type) + def play(url, mime_type, timeout=5): + self.chromecast.play_media(url, mime_type) + muted = self.chromecast.status.volume_muted + volume_level = self.chromecast.status.volume_level logger.info( 'Chromecast status: Volume {volume} ({muted})'.format( - muted='Muted' if cast.is_muted else 'Unmuted', - volume=cast.volume * 100)) - if cast.is_muted: + muted='Muted' if muted else 'Unmuted', + volume=volume_level * 100)) + if muted: logger.info('Unmuting Chromecast ...') - cast.set_mute(False) - if cast.volume != self.desired_volume: + self.chromecast.set_volume_muted(False) + if volume_level != self.desired_volume: logger.info('Setting Chromecast volume to {} ...'.format( self.desired_volume * 100)) - cast.set_volume(self.desired_volume) + self.chromecast..set_volume(self.desired_volume) - def stop(host, port, timeout=5): - cast = pycastv2.MediaPlayerController(host, port, timeout) - cast.stop_application() - cast.disconnect_application() + def stop(timeout=5): + self.chromecast.media_controller.stop() + self.chromecast.quit_app() - play(self.chromecast_host, self.PORT, self.media_url, self.mime_type) + play(self.media_url, self.mime_type) self.wait() - stop(self.chromecast_host, self.PORT) + stop() class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): diff --git a/setup.py b/setup.py index a13c98c5..40c82ac0 100644 --- a/setup.py +++ b/setup.py @@ -41,14 +41,13 @@ "docopt >= 0.6.1", "requests >= 2.2.1", "setproctitle >= 1.1.10", - "protobuf >= 3.0.0", "notify2 >= 0.3", "psutil >= 5.4.7", "chardet >= 3.0.4", "pyroute2 >= 0.3.5", "netifaces >= 0.10.0", "lxml >= 3", - "zeroconf >= 0.17.4", + "pychromecast >= 2.3.0", "PyGObject >= 3.3.0", "dbus-python >= 1.0.0", ], From 4bbae455016d78df77866698ee686a308ff5069e Mon Sep 17 00:00:00 2001 From: Andreas Harter Date: Tue, 23 Oct 2018 22:51:44 +0200 Subject: [PATCH 06/15] Update README to python3 --- README.md | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6f9016bc..258b3edd 100644 --- a/README.md +++ b/README.md @@ -252,23 +252,21 @@ is loaded. These are the requirements _pulseaudio-dlna_ acutally needs to run. These dependencies will get installed if you install it via the PPA. -- python2.7 -- python-pip -- python-setuptools -- python-dbus -- python-docopt -- python-requests -- python-setproctitle -- python-gi -- python-protobuf -- python-notify2 -- python-psutil -- python-concurrent.futures -- python-chardet -- python-netifaces -- python-pyroute2 | python-netaddr -- python-lxml -- python-zeroconf +- python3 +- python3-pip +- python3-setuptools +- python3-dbus +- python3-docopt +- python3-requests +- python3-setproctitle +- python3-gi +- python3-notify2 +- python3-psutil +- python3-chardet +- python3-netifaces +- python3-pyroute2 | python3-netaddr +- python3-lxml +- python3-pychromecast - vorbis-tools - sox - lame @@ -278,7 +276,7 @@ will get installed if you install it via the PPA. You can install all the dependencies in Ubuntu via: - sudo apt-get install python2.7 python-pip python-setuptools python-dbus python-docopt python-requests python-setproctitle python-gi python-protobuf python-notify2 python-psutil python-concurrent.futures python-chardet python-netifaces python-pyroute2 python-netaddr python-lxml python-zeroconf vorbis-tools sox lame flac faac opus-tools + sudo apt-get install python3 python3-pip python3-setuptools python3-dbus python3-docopt python3-requests python3-setproctitle python3-gi python3-notify2 python3-psutil python3-chardet python3-netifaces python3-pyroute2 python3-netaddr python3-lxml python3-pychromecast vorbis-tools sox lame flac faac opus-tools ### PulseAudio DBus module ### @@ -314,7 +312,7 @@ So all Ubuntu versions prior to _14.10 Utopic_ need to install: All Ubuntu versions above install: - sudo apt-get install virtualenv python-dev + sudo apt-get install virtualenv python3-dev #### Installing & starting #### From 787044f41b7002bfd2b2e3dbd4dbe7c3b46d1cfe Mon Sep 17 00:00:00 2001 From: dadosch Date: Fri, 21 Dec 2018 19:14:14 +0000 Subject: [PATCH 07/15] fix UnicodeDecodeError 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte see also https://stackoverflow.com/questions/42339876/error-unicodedecodeerror-utf-8-codec-cant-decode-byte-0xff-in-position-0-in --- pulseaudio_dlna/images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulseaudio_dlna/images.py b/pulseaudio_dlna/images.py index 0a453f80..ec0f6924 100644 --- a/pulseaudio_dlna/images.py +++ b/pulseaudio_dlna/images.py @@ -99,7 +99,7 @@ def __init__(self, path, cached=True): def _read_data(self): try: - with open(self.path) as h: + with open(self.path, 'rb') as h: self._data = h.read() except EnvironmentError: raise ImageNotAccessible(self.path) From 4a0e92bfa3f881ab205d40559d4b635e040d2116 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Thu, 31 Jan 2019 17:44:50 +0100 Subject: [PATCH 08/15] flake8 --- pulseaudio_dlna/__init__.py | 4 +--- pulseaudio_dlna/__main__.py | 4 +--- pulseaudio_dlna/application.py | 2 -- pulseaudio_dlna/codecs.py | 3 +-- pulseaudio_dlna/covermodes.py | 9 ++++----- pulseaudio_dlna/daemon.py | 2 -- pulseaudio_dlna/encoders/__init__.py | 4 +--- pulseaudio_dlna/encoders/avconv.py | 2 -- pulseaudio_dlna/encoders/ffmpeg.py | 2 -- pulseaudio_dlna/encoders/generic.py | 2 -- pulseaudio_dlna/holder.py | 4 +--- pulseaudio_dlna/images.py | 8 +++----- pulseaudio_dlna/notification.py | 7 +++---- pulseaudio_dlna/plugins/__init__.py | 2 -- pulseaudio_dlna/plugins/chromecast/__init__.py | 8 ++++---- pulseaudio_dlna/plugins/chromecast/renderer.py | 16 ++++++---------- pulseaudio_dlna/plugins/dlna/__init__.py | 4 +--- .../plugins/dlna/pyupnpv2/__init__.py | 8 +++----- pulseaudio_dlna/plugins/dlna/renderer.py | 8 +++----- pulseaudio_dlna/plugins/dlna/ssdp/__init__.py | 2 -- pulseaudio_dlna/plugins/dlna/ssdp/discover.py | 4 +--- pulseaudio_dlna/plugins/dlna/ssdp/listener.py | 4 +--- pulseaudio_dlna/plugins/renderer.py | 6 +++--- pulseaudio_dlna/pulseaudio.py | 8 +++----- pulseaudio_dlna/recorders.py | 2 -- pulseaudio_dlna/rules.py | 7 +++---- pulseaudio_dlna/streamserver.py | 15 ++++++++------- pulseaudio_dlna/utils/encoding.py | 2 -- pulseaudio_dlna/utils/git.py | 2 -- pulseaudio_dlna/utils/network.py | 4 +--- pulseaudio_dlna/utils/psutil.py | 3 --- pulseaudio_dlna/utils/subprocess.py | 3 --- pulseaudio_dlna/workarounds.py | 4 +--- 33 files changed, 53 insertions(+), 112 deletions(-) diff --git a/pulseaudio_dlna/__init__.py b/pulseaudio_dlna/__init__.py index 03a8080b..1ee758a2 100644 --- a/pulseaudio_dlna/__init__.py +++ b/pulseaudio_dlna/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import os import pkg_resources @@ -30,7 +28,7 @@ if os.environ.get('USE_PKG_VERSION', None) == '1': branch, rev = None, None else: - branch, rev = utils.git.get_head_version() + branch, rev = git.get_head_version() __version__ = '{version}{rev}'.format( version=version, diff --git a/pulseaudio_dlna/__main__.py b/pulseaudio_dlna/__main__.py index a5748a7e..86d7d7a7 100644 --- a/pulseaudio_dlna/__main__.py +++ b/pulseaudio_dlna/__main__.py @@ -115,9 +115,6 @@ ''' - - - import sys import os import docopt @@ -170,5 +167,6 @@ def acquire_lock(): except socket.error: return False + if __name__ == "__main__": sys.exit(main()) diff --git a/pulseaudio_dlna/application.py b/pulseaudio_dlna/application.py index 8f3d864a..22e710f3 100644 --- a/pulseaudio_dlna/application.py +++ b/pulseaudio_dlna/application.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import multiprocessing import signal import setproctitle diff --git a/pulseaudio_dlna/codecs.py b/pulseaudio_dlna/codecs.py index f4504301..b1e51675 100644 --- a/pulseaudio_dlna/codecs.py +++ b/pulseaudio_dlna/codecs.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import functools import logging import re @@ -363,4 +361,5 @@ def load_codecs(): CODECS[_type.IDENTIFIER] = _type return None + load_codecs() diff --git a/pulseaudio_dlna/covermodes.py b/pulseaudio_dlna/covermodes.py index d7deb770..8a3992ea 100644 --- a/pulseaudio_dlna/covermodes.py +++ b/pulseaudio_dlna/covermodes.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import sys import inspect import socket @@ -82,7 +80,7 @@ class DefaultCoverMode(BaseCoverMode): def thumb(self): try: return self.bridge.device.get_image_url('default.png') - except: + except Exception: return None @@ -111,7 +109,7 @@ def thumb(self): try: return self.bridge.device.get_image_url( 'distribution-{}.png'.format(dist_icon)) - except: + except Exception: return None @@ -124,7 +122,7 @@ def thumb(self): try: return self.bridge.device.get_sys_icon_url( self.bridge.sink.primary_application_name) - except: + except Exception: return None @@ -136,4 +134,5 @@ def load_modes(): MODES[_type.IDENTIFIER] = _type return None + load_modes() diff --git a/pulseaudio_dlna/daemon.py b/pulseaudio_dlna/daemon.py index 13ef28e1..72a27a3c 100644 --- a/pulseaudio_dlna/daemon.py +++ b/pulseaudio_dlna/daemon.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - from gi.repository import GObject import dbus diff --git a/pulseaudio_dlna/encoders/__init__.py b/pulseaudio_dlna/encoders/__init__.py index f4f57144..7b1ccbe6 100644 --- a/pulseaudio_dlna/encoders/__init__.py +++ b/pulseaudio_dlna/encoders/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import distutils.spawn import inspect import sys @@ -66,7 +64,6 @@ def _find_executable(path): # The distutils module uses python's ascii default encoding and is # therefore not capable of handling unicode properly when it contains # non-ascii characters. - encoding = 'utf-8' result = distutils.spawn.find_executable(path) return result @@ -192,4 +189,5 @@ def load_encoders(): ENCODERS.append(_type) return None + load_encoders() diff --git a/pulseaudio_dlna/encoders/avconv.py b/pulseaudio_dlna/encoders/avconv.py index b19c5070..9d0accbf 100644 --- a/pulseaudio_dlna/encoders/avconv.py +++ b/pulseaudio_dlna/encoders/avconv.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging from pulseaudio_dlna.encoders.ffmpeg import ( diff --git a/pulseaudio_dlna/encoders/ffmpeg.py b/pulseaudio_dlna/encoders/ffmpeg.py index ed94c22a..4a444e82 100644 --- a/pulseaudio_dlna/encoders/ffmpeg.py +++ b/pulseaudio_dlna/encoders/ffmpeg.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging from pulseaudio_dlna.encoders import ( diff --git a/pulseaudio_dlna/encoders/generic.py b/pulseaudio_dlna/encoders/generic.py index 14e174a9..5940f4fa 100644 --- a/pulseaudio_dlna/encoders/generic.py +++ b/pulseaudio_dlna/encoders/generic.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging from pulseaudio_dlna.encoders import ( diff --git a/pulseaudio_dlna/holder.py b/pulseaudio_dlna/holder.py index ded44dd7..f6697caa 100644 --- a/pulseaudio_dlna/holder.py +++ b/pulseaudio_dlna/holder.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging import threading import requests @@ -73,7 +71,7 @@ def search(self, ttl=None, host=None): break if all_dead: break - except: + except Exception: traceback.print_exc() logger.info('Holder.search()') diff --git a/pulseaudio_dlna/images.py b/pulseaudio_dlna/images.py index ec0f6924..92a100e5 100644 --- a/pulseaudio_dlna/images.py +++ b/pulseaudio_dlna/images.py @@ -16,8 +16,6 @@ # along with pulseaudio-dlna. If not, see . - - import tempfile import logging import gi @@ -62,7 +60,7 @@ def get_icon_by_name(name, size=256): try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk - except: + except Exception: raise MissingDependencies( 'Unable to lookup system icons!', ['gir1.2-gtk-3.0'] @@ -123,13 +121,13 @@ def __init__(self, path, cached=True, size=256): try: gi.require_version('Rsvg', '2.0') from gi.repository import Rsvg - except: + except Exception: raise MissingDependencies( 'Unable to convert SVG image to PNG!', ['gir1.2-rsvg-2.0'] ) try: import cairo - except: + except Exception: raise MissingDependencies( 'Unable to convert SVG image to PNG!', ['cairo'] ) diff --git a/pulseaudio_dlna/notification.py b/pulseaudio_dlna/notification.py index 6de13ca2..62e37a57 100644 --- a/pulseaudio_dlna/notification.py +++ b/pulseaudio_dlna/notification.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging import notify2 @@ -29,14 +27,15 @@ def show(title, message, icon=''): notice = notify2.Notification(title, message, icon) notice.set_timeout(notify2.EXPIRES_DEFAULT) notice.show() - except: + except Exception: logger.info( 'notify2 failed to display: {title} - {message}'.format( title=title, message=message)) + try: notify2.init('pulseaudio_dlna') -except: +except Exception: logger.error('notify2 could not be initialized! Notifications will ' 'most likely not work.') diff --git a/pulseaudio_dlna/plugins/__init__.py b/pulseaudio_dlna/plugins/__init__.py index 962c6c6a..3cff717f 100644 --- a/pulseaudio_dlna/plugins/__init__.py +++ b/pulseaudio_dlna/plugins/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import functools diff --git a/pulseaudio_dlna/plugins/chromecast/__init__.py b/pulseaudio_dlna/plugins/chromecast/__init__.py index 51f6e38a..b40887ed 100644 --- a/pulseaudio_dlna/plugins/chromecast/__init__.py +++ b/pulseaudio_dlna/plugins/chromecast/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging import threading @@ -37,7 +35,8 @@ def lookup(self, url, xml): def discover(self, holder, ttl=None, host=None): self.holder = holder - stop_discovery = pychromecast.get_chromecasts(blocking=False, callback=self._on_device_added) + stop_discovery = pychromecast.get_chromecasts( + blocking=False, callback=self._on_device_added) if ttl: t = threading.Timer(ttl, stop_discovery) @@ -46,7 +45,8 @@ def discover(self, holder, ttl=None, host=None): @pulseaudio_dlna.plugins.BasePlugin.add_device_after def _on_device_added(self, device): - return ChromecastRenderer(device, model_number, model_description, manufacturer) + return ChromecastRenderer( + device, model_number, model_description, manufacturer) @pulseaudio_dlna.plugins.BasePlugin.remove_device_after def _on_device_removed(self, device): diff --git a/pulseaudio_dlna/plugins/chromecast/renderer.py b/pulseaudio_dlna/plugins/chromecast/renderer.py index 6b5d4e80..66198d5d 100644 --- a/pulseaudio_dlna/plugins/chromecast/renderer.py +++ b/pulseaudio_dlna/plugins/chromecast/renderer.py @@ -15,12 +15,9 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import requests import logging import urllib.parse -import socket import traceback import lxml @@ -37,10 +34,7 @@ class ChromecastRenderer(pulseaudio_dlna.plugins.renderer.BaseRenderer): - def __init__(self, - chromecast, - model_number=None, - model_description=None): + def __init__(self, chromecast, model_number=None, model_description=None): pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__( self, udn=chromecast.uuid, @@ -74,7 +68,8 @@ def play(self, url=None, codec=None, artist=None, title=None, thumb=None): url = url or self.get_stream_url() try: # TODO: artist missing - self.chromecast.play_media(url, mime_type=self.codec.mime_type, title=title, thumb=thumb) + self.chromecast.play_media( + url, mime_type=self.codec.mime_type, title=title, thumb=thumb) self.state = self.STATE_PLAYING return 200, None except PyChromecastError as e: @@ -171,7 +166,7 @@ def from_xml(cls, url, xml): model_description=None, manufacturer=str(device_manufacturer.text), ) - except: + except Exception as e: logger.error('No valid XML returned from {url}.'.format(url=url)) return None @@ -191,6 +186,7 @@ def from_properties( manufacturer=manufacturer, uuid=udn, cast_type=cast_type, ) chromecast = Chromecast(host=ip, port=port or 8009, device=device) - return ChromecastRenderer(chromecast=chromecast, + return ChromecastRenderer( + chromecast=chromecast, model_number=model_number, model_description=model_description) diff --git a/pulseaudio_dlna/plugins/dlna/__init__.py b/pulseaudio_dlna/plugins/dlna/__init__.py index 42b39e7f..03c28607 100644 --- a/pulseaudio_dlna/plugins/dlna/__init__.py +++ b/pulseaudio_dlna/plugins/dlna/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging import threading import traceback @@ -73,7 +71,7 @@ def launch_listener(): thread.start() for thread in threads: thread.join() - except: + except Exception: traceback.print_exc() logger.info('DLNAPlugin.discover()') diff --git a/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py b/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py index 92bc9cd9..08dba0f9 100644 --- a/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py +++ b/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import requests import urllib.parse import logging @@ -161,7 +159,7 @@ def etree_to_dict(t): try: xml = lxml.etree.fromstring(xml) return etree_to_dict(xml) - except: + except Exception: raise XmlParsingException(xml) @@ -703,13 +701,13 @@ def process_xml(url, xml_root, xml): try: xml_root = lxml.etree.fromstring(xml) return process_xml(url, xml_root, xml) - except: + except Exception: logger.debug('Got broken xml, trying to fix it.') xml = byto.repair_xml(xml) try: xml_root = lxml.etree.fromstring(xml) return process_xml(url, xml_root, xml) - except: + except Exception: import traceback traceback.print_exc() logger.error('No valid XML returned from {url}.'.format( diff --git a/pulseaudio_dlna/plugins/dlna/renderer.py b/pulseaudio_dlna/plugins/dlna/renderer.py index 39d4a6d8..79c9318a 100644 --- a/pulseaudio_dlna/plugins/dlna/renderer.py +++ b/pulseaudio_dlna/plugins/dlna/renderer.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging import time import traceback @@ -171,7 +169,7 @@ def set_volume(self, volume): pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: pass - logger.error('"{}" : {}'.format(self.label, str(e))) + logger.error('"{}" : {}'.format(self.label, str(e))) return None def get_mute(self): @@ -185,7 +183,7 @@ def get_mute(self): pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: pass - logger.error('"{}" : {}'.format(self.label, str(e))) + logger.error('"{}" : {}'.format(self.label, str(e))) return None def set_mute(self, mute): @@ -196,7 +194,7 @@ def set_mute(self, mute): pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: pass - logger.error('"{}" : {}'.format(self.label, str(e))) + logger.error('"{}" : {}'.format(self.label, str(e))) return None def get_mime_types(self): diff --git a/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py b/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py index d2985479..5115f644 100644 --- a/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py +++ b/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import re diff --git a/pulseaudio_dlna/plugins/dlna/ssdp/discover.py b/pulseaudio_dlna/plugins/dlna/ssdp/discover.py index 5c17510b..33828bff 100644 --- a/pulseaudio_dlna/plugins/dlna/ssdp/discover.py +++ b/pulseaudio_dlna/plugins/dlna/ssdp/discover.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import socket import logging import chardet @@ -81,7 +79,7 @@ def search(self, ssdp_ttl=None, ssdp_mx=None, ssdp_amount=None): thread.start() for thread in threads: thread.join() - except: + except Exception: traceback.print_exc() logger.info('SSDPDiscover.search()') diff --git a/pulseaudio_dlna/plugins/dlna/ssdp/listener.py b/pulseaudio_dlna/plugins/dlna/ssdp/listener.py index a9f81af5..350a9bc6 100644 --- a/pulseaudio_dlna/plugins/dlna/ssdp/listener.py +++ b/pulseaudio_dlna/plugins/dlna/ssdp/listener.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - from gi.repository import GObject import socketserver @@ -57,7 +55,7 @@ def _decode(self, data): for encoding in [guess['encoding'], 'utf-8', 'ascii']: try: return data.decode(encoding) - except: + except Exception: pass logger.error('Could not decode SSDP packet.') return '' diff --git a/pulseaudio_dlna/plugins/renderer.py b/pulseaudio_dlna/plugins/renderer.py index 0da3d3c7..1d99bf07 100644 --- a/pulseaudio_dlna/plugins/renderer.py +++ b/pulseaudio_dlna/plugins/renderer.py @@ -15,12 +15,12 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import re import random import urllib.parse -import urllib.request, urllib.parse, urllib.error +import urllib.request +import urllib.parse +import urllib.error import functools import logging import base64 diff --git a/pulseaudio_dlna/pulseaudio.py b/pulseaudio_dlna/pulseaudio.py index 7c049389..5960a408 100644 --- a/pulseaudio_dlna/pulseaudio.py +++ b/pulseaudio_dlna/pulseaudio.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - from gi.repository import GObject import sys @@ -67,7 +65,7 @@ def _connect(self, signals): dbus_interface='org.freedesktop.DBus.Properties') self.fallback_sink = PulseSinkFactory.new( self.bus, fallback_sink_path) - except: + except Exception: logger.info( 'Could not get default sink. Perhaps there is no one set?') @@ -444,7 +442,7 @@ def stream_client_names(self): for stream in self.streams: try: names.append(stream.client.name) - except: + except Exception: names.append('?') return names @@ -634,7 +632,7 @@ def run(self): def _on_new_message(self, fd, condition): try: message = self.pulse_queue.get_nowait() - except: + except Exception: return True message_type = message.get('type', None) diff --git a/pulseaudio_dlna/recorders.py b/pulseaudio_dlna/recorders.py index 1ac50cbe..bd7ba281 100644 --- a/pulseaudio_dlna/recorders.py +++ b/pulseaudio_dlna/recorders.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import pulseaudio_dlna.codecs diff --git a/pulseaudio_dlna/rules.py b/pulseaudio_dlna/rules.py index 67c9c984..f9a494a2 100644 --- a/pulseaudio_dlna/rules.py +++ b/pulseaudio_dlna/rules.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import functools import logging import inspect @@ -46,7 +44,7 @@ def __eq__(self, other): try: if isinstance(other, str): return type(self) is RULES[other] - except: + except Exception: raise RuleNotFoundException(other) return type(self) is type(other) @@ -56,7 +54,7 @@ def __gt__(self, other): try: if isinstance(other, str): return type(self) > RULES[other] - except: + except Exception: raise RuleNotFoundException() return type(self) > type(other) @@ -155,4 +153,5 @@ def load_rules(): RULES[name] = _type return None + load_rules() diff --git a/pulseaudio_dlna/streamserver.py b/pulseaudio_dlna/streamserver.py index 422a46f0..07a914ff 100644 --- a/pulseaudio_dlna/streamserver.py +++ b/pulseaudio_dlna/streamserver.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - from gi.repository import GObject import re @@ -27,7 +25,9 @@ import select import sys import base64 -import urllib.request, urllib.parse, urllib.error +import urllib.request +import urllib.parse +import urllib.error import json import os import signal @@ -120,10 +120,10 @@ def terminate_processes(processes): try: os.kill(pid, signal.SIGTERM) _pid, return_code = os.waitpid(pid, 0) - except: + except Exception: try: os.kill(pid, signal.SIGKILL) - except: + except Exception: pass chunk_size = self.CHUNK_SIZE @@ -266,7 +266,8 @@ def __str__(self): '\n'.join( [' {}\n {}'.format( path, - ' '.join([str(s) for id, s in list(streams.items())])) + ' '.join( + [str(s) for id, s in list(streams.items())])) for path, streams in list(self.streams.items())], ), ) @@ -461,7 +462,7 @@ def serve_forever(self, poll_interval=0.5): def _on_new_message(self, fd, condition): try: message = self.stream_queue.get_nowait() - except: + except Exception: return True message_type = message.get('type', None) diff --git a/pulseaudio_dlna/utils/encoding.py b/pulseaudio_dlna/utils/encoding.py index 84565951..58a50c77 100644 --- a/pulseaudio_dlna/utils/encoding.py +++ b/pulseaudio_dlna/utils/encoding.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging import sys import locale diff --git a/pulseaudio_dlna/utils/git.py b/pulseaudio_dlna/utils/git.py index 20858540..052adba1 100644 --- a/pulseaudio_dlna/utils/git.py +++ b/pulseaudio_dlna/utils/git.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import os GIT_DIRECTORY = '../../.git/' diff --git a/pulseaudio_dlna/utils/network.py b/pulseaudio_dlna/utils/network.py index 23473d77..9dc8f79e 100644 --- a/pulseaudio_dlna/utils/network.py +++ b/pulseaudio_dlna/utils/network.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import netifaces import traceback import socket @@ -31,7 +29,7 @@ def default_ipv4(): try: default_if = netifaces.gateways()['default'][netifaces.AF_INET][1] return netifaces.ifaddresses(default_if)[netifaces.AF_INET][0]['addr'] - except: + except Exception: traceback.print_exc() return None diff --git a/pulseaudio_dlna/utils/psutil.py b/pulseaudio_dlna/utils/psutil.py index 793f4709..1259988f 100644 --- a/pulseaudio_dlna/utils/psutil.py +++ b/pulseaudio_dlna/utils/psutil.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - - import logging import psutil diff --git a/pulseaudio_dlna/utils/subprocess.py b/pulseaudio_dlna/utils/subprocess.py index 48e0d00e..c6c22de0 100644 --- a/pulseaudio_dlna/utils/subprocess.py +++ b/pulseaudio_dlna/utils/subprocess.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - - from gi.repository import GObject import subprocess diff --git a/pulseaudio_dlna/workarounds.py b/pulseaudio_dlna/workarounds.py index 9af928b0..7a74a78d 100644 --- a/pulseaudio_dlna/workarounds.py +++ b/pulseaudio_dlna/workarounds.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see . - - import logging from lxml import etree import requests @@ -115,7 +113,7 @@ def __init__(self, xml): if (not self._detect_remotecontrolinterface(xml)): raise Exception() self.enabled = True - except: + except Exception: logger.warning( 'The YamahaWorkaround initialization failed. ' 'Automatic source switching will not be enabled' From fb83486d164b4e2f343f0ba8d65d1cbff098c714 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Thu, 31 Jan 2019 19:27:36 +0100 Subject: [PATCH 09/15] Fixed the Chromecast plugin --- pulseaudio_dlna/application.py | 1 - .../plugins/chromecast/__init__.py | 4 +- .../plugins/chromecast/renderer.py | 116 +++++++++--------- 3 files changed, 58 insertions(+), 63 deletions(-) diff --git a/pulseaudio_dlna/application.py b/pulseaudio_dlna/application.py index 22e710f3..215356bb 100644 --- a/pulseaudio_dlna/application.py +++ b/pulseaudio_dlna/application.py @@ -31,7 +31,6 @@ import pulseaudio_dlna.plugins.dlna.ssdp.listener import pulseaudio_dlna.plugins.dlna.ssdp.discover import pulseaudio_dlna.plugins.chromecast -# import pulseaudio_dlna.plugins.chromecast.mdns import pulseaudio_dlna.encoders import pulseaudio_dlna.covermodes import pulseaudio_dlna.streamserver diff --git a/pulseaudio_dlna/plugins/chromecast/__init__.py b/pulseaudio_dlna/plugins/chromecast/__init__.py index b40887ed..3595a261 100644 --- a/pulseaudio_dlna/plugins/chromecast/__init__.py +++ b/pulseaudio_dlna/plugins/chromecast/__init__.py @@ -37,7 +37,6 @@ def discover(self, holder, ttl=None, host=None): self.holder = holder stop_discovery = pychromecast.get_chromecasts( blocking=False, callback=self._on_device_added) - if ttl: t = threading.Timer(ttl, stop_discovery) t.start() @@ -45,8 +44,7 @@ def discover(self, holder, ttl=None, host=None): @pulseaudio_dlna.plugins.BasePlugin.add_device_after def _on_device_added(self, device): - return ChromecastRenderer( - device, model_number, model_description, manufacturer) + return ChromecastRendererFactory.from_pychromecast(device) @pulseaudio_dlna.plugins.BasePlugin.remove_device_after def _on_device_removed(self, device): diff --git a/pulseaudio_dlna/plugins/chromecast/renderer.py b/pulseaudio_dlna/plugins/chromecast/renderer.py index 66198d5d..6950a569 100644 --- a/pulseaudio_dlna/plugins/chromecast/renderer.py +++ b/pulseaudio_dlna/plugins/chromecast/renderer.py @@ -20,34 +20,33 @@ import urllib.parse import traceback import lxml +import pychromecast import pulseaudio_dlna.plugins.renderer import pulseaudio_dlna.rules import pulseaudio_dlna.codecs -from pychromecast import Chromecast -from pychromecast.dial import DeviceStatus, CAST_TYPES, CAST_TYPE_CHROMECAST -from pychromecast.error import PyChromecastError logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast.renderer') class ChromecastRenderer(pulseaudio_dlna.plugins.renderer.BaseRenderer): - def __init__(self, chromecast, model_number=None, model_description=None): + def __init__( + self, name, ip, port, udn, model_name, model_number, + model_description, manufacturer): pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__( self, - udn=chromecast.uuid, + udn=udn, flavour='Chromecast', - name=chromecast.name, - ip=chromecast.host, - port=chromecast.port, - model_name=chromecast.model_name, + name=name, + ip=ip, + port=port or 8009, + model_name=model_name, model_number=model_number, model_description=model_description, - manufacturer=chromecast.device.manufacturer + manufacturer=manufacturer ) - self.chromecast = chromecast def activate(self, config): if config: @@ -63,16 +62,27 @@ def activate(self, config): self.apply_device_rules() self.prioritize_codecs() + def _create_pychromecast(self): + chromecast = pychromecast._get_chromecast_from_host( + (self.ip, self.port, self.udn, self.model_name, self.name)) + return chromecast + def play(self, url=None, codec=None, artist=None, title=None, thumb=None): self._before_play() url = url or self.get_stream_url() try: - # TODO: artist missing - self.chromecast.play_media( - url, mime_type=self.codec.mime_type, title=title, thumb=thumb) + chromecast = self._create_pychromecast() + chromecast.media_controller.play_media( + url, + content_type=self.codec.mime_type, + title=title, + thumb=thumb, + stream_type=pychromecast.controllers.media.STREAM_TYPE_LIVE, + autoplay=True, + ) self.state = self.STATE_PLAYING return 200, None - except PyChromecastError as e: + except pychromecast.error.PyChromecastError as e: return 500, str(e) except (pulseaudio_dlna.plugins.renderer.NoEncoderFoundException, pulseaudio_dlna.plugins.renderer.NoSuitableHostFoundException) as e: @@ -87,10 +97,10 @@ def stop(self): self._before_stop() try: self.state = self.STATE_STOPPED - self.chromecast.stop() - self.chromecast.disconnect() + chromecast = self._create_pychromecast() + chromecast.quit_app() return 200, None - except PyChromecastError as e: + except pychromecast.error.PyChromecastError as e: return 500, e except Exception: traceback.print_exc() @@ -105,14 +115,7 @@ def pause(self): class ChromecastRendererFactory(object): NOTIFICATION_TYPES = [ - 'urn:dial-multiscreen-org:device:dial:1', - ] - - CHROMECAST_MODELS = [ - 'Eureka Dongle', - 'Chromecast Audio', - 'Nexus Player', - 'Freebox Player Mini', + 'urn:dial-multiscreen-org:device:dial:', ] @classmethod @@ -146,27 +149,25 @@ def from_xml(cls, url, xml): device_modelname = device.find('{*}modelName') device_manufacturer = device.find('{*}manufacturer') - if device_type.text not in cls.NOTIFICATION_TYPES: - continue - - if device_modelname.text.strip() not in cls.CHROMECAST_MODELS: - logger.info( - 'The Chromecast seems not to be an original one. ' - 'Model name: "{}" Skipping device ...'.format( - device_modelname.text)) - return None - - return cls.from_properties( - name=str(device_friendlyname.text), - ip=str(ip), - port=None, - udn=str(device_udn.text), - model_name=str(device_modelname.text), - model_number=None, - model_description=None, - manufacturer=str(device_manufacturer.text), - ) + valid_notification_type = False + for notification_type in cls.NOTIFICATION_TYPES: + if device_type.text.startswith(notification_type): + valid_notification_type = True + break + + if valid_notification_type: + return ChromecastRenderer( + name=str(device_friendlyname.text), + ip=str(ip), + port=None, + udn=str(device_udn.text), + model_name=str(device_modelname.text), + model_number=None, + model_description=None, + manufacturer=str(device_manufacturer.text), + ) except Exception as e: + traceback.print_exc() logger.error('No valid XML returned from {url}.'.format(url=url)) return None @@ -176,17 +177,14 @@ def from_header(cls, header): return cls.from_url(header['location']) @classmethod - def from_properties( - cls, name, ip, port, udn, model_name, model_number, - model_description, manufacturer): - cast_type = CAST_TYPES.get(model_name.lower(), - CAST_TYPE_CHROMECAST) - device = DeviceStatus( - friendly_name=name, model_name=model_name, - manufacturer=manufacturer, uuid=udn, cast_type=cast_type, - ) - chromecast = Chromecast(host=ip, port=port or 8009, device=device) + def from_pychromecast(self, pychromecast): return ChromecastRenderer( - chromecast=chromecast, - model_number=model_number, - model_description=model_description) + name=pychromecast.name, + ip=pychromecast.host, + port=pychromecast.port, + udn='uuid:{}'.format(pychromecast.uuid), + model_name=pychromecast.model_name, + model_number=None, + model_description=None, + manufacturer=pychromecast.device.manufacturer, + ) From 59ee1419b98b671807ef6fb0b9bbca7aaba0e061 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Thu, 31 Jan 2019 19:32:00 +0100 Subject: [PATCH 10/15] Removed unused imports --- pulseaudio_dlna/plugins/renderer.py | 3 --- pulseaudio_dlna/streamserver.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/pulseaudio_dlna/plugins/renderer.py b/pulseaudio_dlna/plugins/renderer.py index 1d99bf07..923a51fa 100644 --- a/pulseaudio_dlna/plugins/renderer.py +++ b/pulseaudio_dlna/plugins/renderer.py @@ -18,9 +18,6 @@ import re import random import urllib.parse -import urllib.request -import urllib.parse -import urllib.error import functools import logging import base64 diff --git a/pulseaudio_dlna/streamserver.py b/pulseaudio_dlna/streamserver.py index 07a914ff..f431462f 100644 --- a/pulseaudio_dlna/streamserver.py +++ b/pulseaudio_dlna/streamserver.py @@ -25,9 +25,7 @@ import select import sys import base64 -import urllib.request import urllib.parse -import urllib.error import json import os import signal From b3fb68f494eff1a623a7c2209495b17d7b1a89f4 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Thu, 31 Jan 2019 20:39:27 +0100 Subject: [PATCH 11/15] Fixed and updated chomecast-beam --- scripts/chromecast-beam.py | 168 ++++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 68 deletions(-) diff --git a/scripts/chromecast-beam.py b/scripts/chromecast-beam.py index 45602a58..8af1bfe6 100755 --- a/scripts/chromecast-beam.py +++ b/scripts/chromecast-beam.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/python # -*- coding: utf-8 -*- # This file is part of pulseaudio-dlna. @@ -30,6 +30,7 @@ [--transcode-audio ] [--start-time=] [--sub-titles] + [--debug] Options: @@ -110,8 +111,6 @@ ''' - - import docopt import logging import os @@ -125,11 +124,12 @@ import shutil import traceback import re +import errno import http.server import socketserver +import pychromecast import pulseaudio_dlna.utils.network -from pychromecast import Chromecast logger = logging.getLogger('chromecast-beam') @@ -140,12 +140,18 @@ class StoppableThread(threading.Thread): + MODE_IMMEDIATE = 'immediate' + def __init__(self, *args, **kwargs): threading.Thread.__init__(self, *args, **kwargs) self.stop_event = threading.Event() + self.stop_mode = None - def stop(self): - self.stop_event.set() + def stop(self, immediate=False): + if not self.is_stopped: + if immediate: + self.stop_mode = immediate + self.stop_event.set() def wait(self): self.stop_event.wait() @@ -162,35 +168,33 @@ class ChromecastThread(StoppableThread): def __init__(self, chromecast_host, media_url, mime_type=None, *args, **kwargs): StoppableThread.__init__(self, *args, **kwargs) + self.chromecast_host = chromecast_host self.media_url = media_url self.mime_type = mime_type or 'video/mp4' self.desired_volume = 1.0 - self.chromecast = Chromecast(host=chromecast_host, port=PORT, timeout=5) + self.timeout = 5 def run(self): - def play(url, mime_type, timeout=5): - self.chromecast.play_media(url, mime_type) - muted = self.chromecast.status.volume_muted - volume_level = self.chromecast.status.volume_level - logger.info( - 'Chromecast status: Volume {volume} ({muted})'.format( - muted='Muted' if muted else 'Unmuted', - volume=volume_level * 100)) - if muted: - logger.info('Unmuting Chromecast ...') - self.chromecast.set_volume_muted(False) - if volume_level != self.desired_volume: - logger.info('Setting Chromecast volume to {} ...'.format( - self.desired_volume * 100)) - self.chromecast..set_volume(self.desired_volume) - - def stop(timeout=5): - self.chromecast.media_controller.stop() - self.chromecast.quit_app() - - play(self.media_url, self.mime_type) + chromecast = pychromecast.Chromecast( + host=chromecast_host, port=self.PORT, timeout=self.timeout) + + chromecast.media_controller.play_media( + self.media_url, + content_type=self.mime_type, + stream_type=pychromecast.controllers.media.STREAM_TYPE_LIVE, + autoplay=True, + ) + + logger.info( + 'Chromecast status: Volume {volume} ({muted})'.format( + muted='Muted' + if chromecast.media_controller.status.volume_muted + else 'Unmuted', + volume=chromecast.media_controller.status.volume_level * 100)) + self.wait() - stop() + if self.stop_mode != StoppableThread.MODE_IMMEDIATE: + chromecast.quit_app() class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): @@ -222,6 +226,8 @@ def handle_error(self, *args, **kwargs): class EncoderSettings(object): + ENCODER_BINARY = '/usr/bin/vlc' + TRANSCODE_USED = False TRANSCODE_VIDEO_CODEC = None @@ -238,10 +244,13 @@ class EncoderSettings(object): AUDIO_TRACK = None AUDIO_TRACK_ID = None + TRANSCODE_VIDEO_DEFAULTS_SET = False TRANSCODE_VIDEO_CODEC_DEF = 'h264' - TRANSCODE_VIDEO_BITRATE_DEF = 800 - TRANSCODE_VIDEO_SCALE_DEF = 'Automatisch' + TRANSCODE_VIDEO_BITRATE_DEF = 3000 + TRANSCODE_VIDEO_SCALE_DEF = 'Auto' + TRANSCODE_VIDEO_MUX_DEF = 'mkv' + TRANSCODE_AUDIO_DEFAULTS_SET = False TRANSCODE_AUDIO_CODEC_DEF = 'mp3' TRANSCODE_AUDIO_BITRATE_DEF = 192 TRANSCODE_AUDIO_CHANNELS_DEF = 2 @@ -259,7 +268,7 @@ def _decode_settings(cls, settings): k, v = setting.split('=') data[k] = v return data - except: + except Exception: return {} @classmethod @@ -270,12 +279,15 @@ def _apply_option(cls, attribute, value): @classmethod def _apply_options(cls, options, option_map): - for option, value in list(cls._decode_settings(options).items()): + for option, value in cls._decode_settings(options).items(): attribute = option_map.get(option, None) cls._apply_option(attribute, value) @classmethod def set_video_defaults(cls): + if cls.TRANSCODE_VIDEO_DEFAULTS_SET: + return + cls.TRANSCODE_VIDEO_DEFAULTS_SET = True cls._apply_option( 'TRANSCODE_VIDEO_CODEC', cls.TRANSCODE_VIDEO_CODEC_DEF) cls._apply_option( @@ -285,6 +297,9 @@ def set_video_defaults(cls): @classmethod def set_audio_defaults(cls): + if cls.TRANSCODE_AUDIO_DEFAULTS_SET: + return + cls.TRANSCODE_AUDIO_DEFAULTS_SET = True cls._apply_option( 'TRANSCODE_AUDIO_CODEC', cls.TRANSCODE_AUDIO_CODEC_DEF) cls._apply_option( @@ -297,6 +312,29 @@ def set_audio_defaults(cls): @classmethod def set_options(cls, options): used = False + if options.get('--start-time', None): + used = True + cls.TRANSCODE_USED = True + cls.set_video_defaults() + cls._apply_option('START_TIME', options['--start-time']) + if options.get('--sub-titles', None): + used = True + cls._apply_option('SUB_TITLES', True) + if options.get('--audio-track', None): + used = True + cls.TRANSCODE_USED = True + cls.set_audio_defaults() + cls._apply_option('AUDIO_TRACK', options['--audio-track']) + if options.get('--audio-language', None): + used = True + cls.TRANSCODE_USED = True + cls.set_audio_defaults() + cls._apply_option('AUDIO_LANGUAGE', options['--audio-language']) + if options.get('--audio-track-id', None): + used = True + cls.TRANSCODE_USED = True + cls.set_audio_defaults() + cls._apply_option('AUDIO_TRACK_ID', options['--audio-track-id']) if options.get('--transcode', None): if options['--transcode'] in ['both', 'audio', 'video']: used = True @@ -339,21 +377,6 @@ def set_options(cls, options): } cls.set_audio_defaults() cls._apply_options(options['--transcode-audio'], option_map) - if options.get('--audio-language', None): - used = True - cls._apply_option('AUDIO_LANGUAGE', options['--audio-language']) - if options.get('--audio-track', None): - used = True - cls._apply_option('AUDIO_TRACK', options['--audio-track']) - if options.get('--audio-track-id', None): - used = True - cls._apply_option('AUDIO_TRACK_ID', options['--audio-track-id']) - if options.get('--start-time', None): - used = True - cls._apply_option('START_TIME', options['--start-time']) - if options.get('--sub-titles', None): - used = True - cls._apply_option('SUB_TITLES', True) return used @@ -381,13 +404,15 @@ def _transcode_cmd_str(cls): if cls.SUB_TITLES: options['soverlay'] = None return ','.join([ - '{}={}'.format(k, v) if v else k for k, v in list(options.items()) + '{}={}'.format(k, v) if v else k for k, v in options.items() ]) @classmethod def command(cls, file_path): command = [ - 'cvlc', file_path, + cls.ENCODER_BINARY, + '--intf', 'dummy', + file_path, ':play-and-exit', ':no-sout-all', ] @@ -402,11 +427,11 @@ def command(cls, file_path): if cls.TRANSCODE_USED: return command + [ ':sout=#transcode{' + cls._transcode_cmd_str() + '}' - ':std{access=file,mux=mkv,dst=-}', + ':std{access=file,mux=' + cls.TRANSCODE_VIDEO_MUX_DEF + ',dst=-}', ] else: return command + [ - ':sout=#file{access=file,mux=mkv,dst=-}', + ':sout=#file{access=file,mux=' + cls.TRANSCODE_VIDEO_MUX_DEF + ',dst=-}' ] @@ -424,29 +449,36 @@ def log_request(self, code='-', size='-'): class TranscodeRequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): - client_address = self.client_address[0] - logger.info('Serving transcoded media file to {} ...'.format( - client_address)) + try: + client_address = self.client_address[0] + logger.info('Serving transcoded media file to {} ...'.format( + client_address)) - self.send_head() - path = self.translate_path(self.path) - command = VLCEncoderSettings.command(path) - logger.info('Launching {}'.format(command)) + self.send_head() + path = self.translate_path(self.path) + command = VLCEncoderSettings.command(path) + logger.info('Launching {}'.format(command)) - try: with open(os.devnull, 'w') as dev_null: encoder_process = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=dev_null) + command, + stdout=subprocess.PIPE, + stderr=sys.stdout.fileno()) shutil.copyfileobj(encoder_process.stdout, self.wfile) - except: - logger.info('Connection from {} closed.'.format(client_address)) - logger.debug(traceback.format_exc()) + except IOError as e: + if e.errno == errno.EPIPE: + logger.info( + 'Connection from {} closed.'.format(client_address)) + else: + traceback.print_exc() + except Exception: + traceback.print_exc() finally: pid = encoder_process.pid logger.info('Terminating process {}'.format(pid)) try: os.kill(pid, signal.SIGKILL) - except: + except Exception: pass def log_request(self, code='-', size='-'): @@ -463,7 +495,7 @@ def get_external_ip(): # Local pulseaudio-dlna installations running in a virutalenv should run this # script as module: -# python3 -m scripts/chromecast-beam 192.168.1.10 ~/videos/test.mkv +# python3 -m scripts.chromecast-beam 192.168.1.10 ~/videos/test.mkv if __name__ == "__main__": @@ -531,7 +563,7 @@ def get_external_ip(): try: http_server.serve_forever() except KeyboardInterrupt: - pass - finally: chromecast_thread.stop() + finally: + chromecast_thread.stop(immediate=True) chromecast_thread.join() From 0607821baf861f97fc8591d48992791074cd7dd5 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Thu, 31 Jan 2019 20:58:40 +0100 Subject: [PATCH 12/15] Fixed the root mode --- pulseaudio_dlna/daemon.py | 3 +++ pulseaudio_dlna/utils/subprocess.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pulseaudio_dlna/daemon.py b/pulseaudio_dlna/daemon.py index 72a27a3c..e86b4b5c 100644 --- a/pulseaudio_dlna/daemon.py +++ b/pulseaudio_dlna/daemon.py @@ -248,6 +248,9 @@ def __eq__(self, other): def __gt__(self, other): return self.pid > other.pid + def __hash__(self): + return self.pid + class PulseAudioFinder(object): @staticmethod diff --git a/pulseaudio_dlna/utils/subprocess.py b/pulseaudio_dlna/utils/subprocess.py index c6c22de0..643dff39 100644 --- a/pulseaudio_dlna/utils/subprocess.py +++ b/pulseaudio_dlna/utils/subprocess.py @@ -72,7 +72,7 @@ def __init__(self, *args, **kwargs): pipe, GObject.IO_IN | GObject.IO_PRI, self._on_new_data) def _on_new_data(self, fd, condition): - line = fd.readline() + line = fd.readline().decode('utf-8') sys.stdout.write(line) sys.stdout.flush() return True From 5e795f21d48eb77b45f1b827fa83bc7184843345 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Tue, 5 Feb 2019 08:23:03 +0100 Subject: [PATCH 13/15] Fix exception handling in DLNA renderer --- pulseaudio_dlna/plugins/dlna/renderer.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pulseaudio_dlna/plugins/dlna/renderer.py b/pulseaudio_dlna/plugins/dlna/renderer.py index 79c9318a..16aad4fb 100644 --- a/pulseaudio_dlna/plugins/dlna/renderer.py +++ b/pulseaudio_dlna/plugins/dlna/renderer.py @@ -153,12 +153,12 @@ def get_volume(self): return int(d['GetVolumeResponse']['CurrentVolume']) except KeyError: e = MissingAttributeException('get_protocol_info') + logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: - pass - logger.error('"{}" : {}'.format(self.label, str(e))) + logger.error('"{}" : {}'.format(self.label, str(e))) return None def set_volume(self, volume): @@ -168,7 +168,6 @@ def set_volume(self, volume): pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: - pass logger.error('"{}" : {}'.format(self.label, str(e))) return None @@ -182,7 +181,6 @@ def get_mute(self): pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: - pass logger.error('"{}" : {}'.format(self.label, str(e))) return None @@ -193,7 +191,6 @@ def set_mute(self, mute): pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: - pass logger.error('"{}" : {}'.format(self.label, str(e))) return None @@ -209,12 +206,12 @@ def get_mime_types(self): return mime_types except KeyError: e = MissingAttributeException('get_protocol_info') + logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: - pass - logger.error('"{}" : {}'.format(self.label, str(e))) + logger.error('"{}" : {}'.format(self.label, str(e))) return None def get_transport_state(self): @@ -224,11 +221,11 @@ def get_transport_state(self): return state except KeyError: e = MissingAttributeException('get_transport_state') + logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: - pass - logger.error('"{}" : {}'.format(self.label, str(e))) + logger.error('"{}" : {}'.format(self.label, str(e))) return None def get_position_info(self): @@ -238,12 +235,12 @@ def get_position_info(self): return state except KeyError: e = MissingAttributeException('get_position_info') + logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: - pass - logger.error('"{}" : {}'.format(self.label, str(e))) + logger.error('"{}" : {}'.format(self.label, str(e))) return None def _update_current_state(self): From b0db8137224f5a293329a60187365168304c3768 Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Sat, 9 Feb 2019 12:42:50 +0100 Subject: [PATCH 14/15] Ensure that provided URLs of DLNA devices are absolute - Fixes an issue with Samsung TVs --- .../plugins/dlna/pyupnpv2/__init__.py | 32 +++++++++++++------ pulseaudio_dlna/plugins/dlna/renderer.py | 1 + 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py b/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py index 08dba0f9..8b2eba6e 100644 --- a/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py +++ b/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py @@ -19,6 +19,8 @@ import urllib.parse import logging import collections +import urllib.parse +import os import lxml import lxml.builder @@ -205,16 +207,19 @@ def __str__(self): class UpnpServiceFactory(object): @classmethod - def from_dict(cls, ip, port, service, request): + def from_dict(cls, ip, port, service, access_url, request): if service['service_type'].startswith( '{}:'.format(SERVICE_TYPE_AVTRANSPORT)): - return UpnpAVTransportService(ip, port, service, request) + return UpnpAVTransportService( + ip, port, service, access_url, request) elif service['service_type'].startswith( '{}:'.format(SERVICE_TYPE_CONNECTION_MANAGER)): - return UpnpConnectionManagerService(ip, port, service, request) + return UpnpConnectionManagerService( + ip, port, service, access_url, request) elif service['service_type'].startswith( '{}:'.format(SERVICE_TYPE_RENDERING_CONTROL)): - return UpnpRenderingControlService(ip, port, service, request) + return UpnpRenderingControlService( + ip, port, service, access_url, request) else: raise UnsupportedServiceTypeException(service['service_type']) @@ -224,20 +229,29 @@ class UpnpService(object): ENCODING = 'utf-8' TIMEOUT = 10 - def __init__(self, ip, port, service, request=None): + def __init__(self, ip, port, service, access_url, request=None): self.ip = ip self.port = port self.supported_actions = [] + self.access_url = access_url self._request = request or requests self._service_type = service['service_type'] - self._control_url = service['control_url'] - self._event_url = service['eventsub_url'] - self._scpd_url = service['scpd_url'] + self._control_url = self._ensure_absolute_url(service['control_url']) + self._event_url = self._ensure_absolute_url(service['eventsub_url']) + self._scpd_url = self._ensure_absolute_url(service['scpd_url']) self._update_supported_actions() + def _ensure_absolute_url(self, url): + if not url.startswith('/'): + url_object = urllib.parse.urlparse(self.access_url) + access_url_path = os.path.dirname(url_object.path) + return os.path.join('/', access_url_path, url) + else: + return url + def _update_supported_actions(self): self.supported_actions = [] response = self._request.get(self.scpd_url) @@ -535,7 +549,7 @@ def __init__(self, description_xml, access_url, ip, port, name, udn, for service in services: try: service = UpnpServiceFactory.from_dict( - ip, port, service, self._request) + ip, port, service, access_url, self._request) if isinstance(service, UpnpAVTransportService): self.av_transport = service if isinstance(service, UpnpConnectionManagerService): diff --git a/pulseaudio_dlna/plugins/dlna/renderer.py b/pulseaudio_dlna/plugins/dlna/renderer.py index 16aad4fb..0b8de0d9 100644 --- a/pulseaudio_dlna/plugins/dlna/renderer.py +++ b/pulseaudio_dlna/plugins/dlna/renderer.py @@ -177,6 +177,7 @@ def get_mute(self): return int(d['GetMuteResponse']['CurrentMute']) != 0 except KeyError: e = MissingAttributeException('get_mute') + logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, From c87584ca34067d7eb92f1cec6deb4429aa1373fa Mon Sep 17 00:00:00 2001 From: Maximilian Joecks Date: Mon, 27 Jan 2020 02:36:01 +0100 Subject: [PATCH 15/15] Fix discovery error " urn:microsoft-com:wmc-1-0" replaced by "urn:microsoft-com:wmc-1-0" --- pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py b/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py index 8b2eba6e..03cfda02 100644 --- a/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py +++ b/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py @@ -713,6 +713,7 @@ def process_xml(url, xml_root, xml): 'Device skipped! \n{}'.format( device_friendlyname.text, e.xml)) try: + xml = xml.decode("utf-8").replace(" urn:microsoft-com:wmc-1-0", "urn:microsoft-com:wmc-1-0").encode("utf-8") xml_root = lxml.etree.fromstring(xml) return process_xml(url, xml_root, xml) except Exception: