From 56dc8d0972ef580e3948daea06a33c1856464e51 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 7 May 2023 19:38:40 +0200 Subject: [PATCH] Migrated the webapp to Tornado. It was just too painful to find a combination of versions of gunicorn, gevent, eventlet, pyuwsgi etc. that could work on all of my systems. On the other hand, Tornado works out of the box with no headaches. Also in this commit: - Updated a bunch of outdated/required integration dependencies. - Black'd and LINTed a couple of old plugins. --- docs/source/conf.py | 1 + .../backend/assistant/google/__init__.py | 81 +++-- .../backend/assistant/google/manifest.yaml | 1 + platypush/backend/camera/pi/manifest.yaml | 2 + platypush/backend/http/__init__.py | 40 ++- .../backend/http/app/routes/websocket.py | 65 ---- platypush/backend/http/ws.py | 76 ++++- platypush/backend/http/wsgi/__init__.py | 28 -- platypush/backend/nfc/__init__.py | 107 +++++-- platypush/backend/nfc/manifest.yaml | 3 +- .../backend/weather/buienradar/manifest.yaml | 3 +- .../assistant/google/pushtotalk/__init__.py | 162 ++++++---- .../assistant/google/pushtotalk/manifest.yaml | 1 + .../plugins/camera/gstreamer/__init__.py | 37 ++- .../plugins/camera/gstreamer/manifest.yaml | 1 + .../plugins/google/calendar/manifest.yaml | 1 + platypush/plugins/google/drive/manifest.yaml | 1 + platypush/plugins/google/fit/manifest.yaml | 1 + platypush/plugins/google/mail/manifest.yaml | 1 + platypush/plugins/google/maps/manifest.yaml | 1 + platypush/plugins/google/pubsub/manifest.yaml | 1 + .../plugins/google/translate/manifest.yaml | 1 + .../plugins/google/youtube/manifest.yaml | 1 + platypush/plugins/media/__init__.py | 287 ++++++++++++----- platypush/plugins/media/gstreamer/__init__.py | 42 +-- .../plugins/media/gstreamer/manifest.yaml | 2 + platypush/plugins/printer/cups/manifest.yaml | 2 + platypush/plugins/sensor/ltr559/__init__.py | 1 + platypush/plugins/sensor/ltr559/manifest.yaml | 1 + platypush/plugins/torrent/__init__.py | 295 +++++++++++------- platypush/plugins/torrent/manifest.yaml | 2 +- requirements.txt | 6 +- setup.py | 21 +- 33 files changed, 837 insertions(+), 438 deletions(-) delete mode 100644 platypush/backend/http/app/routes/websocket.py delete mode 100644 platypush/backend/http/wsgi/__init__.py diff --git a/docs/source/conf.py b/docs/source/conf.py index 900c617c..b1b9a0a6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -304,6 +304,7 @@ autodoc_mock_imports = [ 'TheengsDecoder', 'simple_websocket', 'uvicorn', + 'websockets', ] sys.path.insert(0, os.path.abspath('../..')) diff --git a/platypush/backend/assistant/google/__init__.py b/platypush/backend/assistant/google/__init__.py index 0b763e4e..1fe7fb31 100644 --- a/platypush/backend/assistant/google/__init__.py +++ b/platypush/backend/assistant/google/__init__.py @@ -8,11 +8,22 @@ import os import time from platypush.backend.assistant import AssistantBackend -from platypush.message.event.assistant import \ - ConversationStartEvent, ConversationEndEvent, ConversationTimeoutEvent, \ - ResponseEvent, NoResponseEvent, SpeechRecognizedEvent, AlarmStartedEvent, \ - AlarmEndEvent, TimerStartedEvent, TimerEndEvent, AlertStartedEvent, \ - AlertEndEvent, MicMutedEvent, MicUnmutedEvent +from platypush.message.event.assistant import ( + ConversationStartEvent, + ConversationEndEvent, + ConversationTimeoutEvent, + ResponseEvent, + NoResponseEvent, + SpeechRecognizedEvent, + AlarmStartedEvent, + AlarmEndEvent, + TimerStartedEvent, + TimerEndEvent, + AlertStartedEvent, + AlertEndEvent, + MicMutedEvent, + MicUnmutedEvent, +) class AssistantGoogleBackend(AssistantBackend): @@ -57,22 +68,30 @@ class AssistantGoogleBackend(AssistantBackend): * **google-assistant-library** (``pip install google-assistant-library``) * **google-assistant-sdk[samples]** (``pip install google-assistant-sdk[samples]``) + * **google-auth** (``pip install google-auth``) + """ - def __init__(self, - credentials_file=os.path.join( - os.path.expanduser('~/.config'), - 'google-oauthlib-tool', 'credentials.json'), - device_model_id='Platypush', **kwargs): + _default_credentials_file = os.path.join( + os.path.expanduser('~/.config'), 'google-oauthlib-tool', 'credentials.json' + ) + + def __init__( + self, + credentials_file=_default_credentials_file, + device_model_id='Platypush', + **kwargs + ): """ - :param credentials_file: Path to the Google OAuth credentials file \ - (default: ~/.config/google-oauthlib-tool/credentials.json). \ - See https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials \ + :param credentials_file: Path to the Google OAuth credentials file + (default: ~/.config/google-oauthlib-tool/credentials.json). + See + https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials for instructions to get your own credentials file. :type credentials_file: str - :param device_model_id: Device model ID to use for the assistant \ + :param device_model_id: Device model ID to use for the assistant (default: Platypush) :type device_model_id: str """ @@ -102,17 +121,23 @@ class AssistantGoogleBackend(AssistantBackend): self.bus.post(ConversationTimeoutEvent(assistant=self)) elif event.type == EventType.ON_NO_RESPONSE: self.bus.post(NoResponseEvent(assistant=self)) - elif hasattr(EventType, 'ON_RENDER_RESPONSE') and \ - event.type == EventType.ON_RENDER_RESPONSE: - self.bus.post(ResponseEvent(assistant=self, response_text=event.args.get('text'))) + elif ( + hasattr(EventType, 'ON_RENDER_RESPONSE') + and event.type == EventType.ON_RENDER_RESPONSE + ): + self.bus.post( + ResponseEvent(assistant=self, response_text=event.args.get('text')) + ) tts, args = self._get_tts_plugin() if tts and 'text' in event.args: self.stop_conversation() tts.say(text=event.args['text'], **args) - elif hasattr(EventType, 'ON_RESPONDING_STARTED') and \ - event.type == EventType.ON_RESPONDING_STARTED and \ - event.args.get('is_error_response', False) is True: + elif ( + hasattr(EventType, 'ON_RESPONDING_STARTED') + and event.type == EventType.ON_RESPONDING_STARTED + and event.args.get('is_error_response', False) is True + ): self.logger.warning('Assistant response error') elif event.type == EventType.ON_RECOGNIZING_SPEECH_FINISHED: phrase = event.args['text'].lower().strip() @@ -144,12 +169,12 @@ class AssistantGoogleBackend(AssistantBackend): self.bus.post(event) def start_conversation(self): - """ Starts an assistant conversation """ + """Starts an assistant conversation""" if self.assistant: self.assistant.start_conversation() def stop_conversation(self): - """ Stops an assistant conversation """ + """Stops an assistant conversation""" if self.assistant: self.assistant.stop_conversation() @@ -177,7 +202,9 @@ class AssistantGoogleBackend(AssistantBackend): super().run() with open(self.credentials_file, 'r') as f: - self.credentials = google.oauth2.credentials.Credentials(token=None, **json.load(f)) + self.credentials = google.oauth2.credentials.Credentials( + token=None, **json.load(f) + ) while not self.should_stop(): self._has_error = False @@ -186,12 +213,16 @@ class AssistantGoogleBackend(AssistantBackend): self.assistant = assistant for event in assistant.start(): if not self.is_detecting(): - self.logger.info('Assistant event received but detection is currently paused') + self.logger.info( + 'Assistant event received but detection is currently paused' + ) continue self._process_event(event) if self._has_error: - self.logger.info('Restarting the assistant after an unrecoverable error') + self.logger.info( + 'Restarting the assistant after an unrecoverable error' + ) time.sleep(5) break diff --git a/platypush/backend/assistant/google/manifest.yaml b/platypush/backend/assistant/google/manifest.yaml index 1668b511..410f231c 100644 --- a/platypush/backend/assistant/google/manifest.yaml +++ b/platypush/backend/assistant/google/manifest.yaml @@ -22,5 +22,6 @@ manifest: pip: - google-assistant-library - google-assistant-sdk[samples] + - google-auth package: platypush.backend.assistant.google type: backend diff --git a/platypush/backend/camera/pi/manifest.yaml b/platypush/backend/camera/pi/manifest.yaml index b48cf3c6..793fcadd 100644 --- a/platypush/backend/camera/pi/manifest.yaml +++ b/platypush/backend/camera/pi/manifest.yaml @@ -3,5 +3,7 @@ manifest: install: pip: - picamera + - numpy + - Pillow package: platypush.backend.camera.pi type: backend diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index 6fc4dab1..922ad781 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -3,13 +3,17 @@ import pathlib import secrets import threading -from multiprocessing import Process, cpu_count +from multiprocessing import Process from typing import Mapping, Optional +from tornado.wsgi import WSGIContainer +from tornado.web import Application, FallbackHandler +from tornado.ioloop import IOLoop + from platypush.backend import Backend from platypush.backend.http.app import application -from platypush.backend.http.ws import events_redis_topic -from platypush.backend.http.wsgi import WSGIApplicationWrapper +from platypush.backend.http.ws import WSEventProxy, events_redis_topic + from platypush.bus.redis import RedisBus from platypush.config import Config from platypush.utils import get_redis @@ -177,6 +181,7 @@ class HttpBackend(Backend): self.server_proc = None self._service_registry_thread = None self.bind_address = bind_address + self._io_loop: Optional[IOLoop] = None if resource_dirs: self.resource_dirs = { @@ -200,6 +205,10 @@ class HttpBackend(Backend): super().on_stop() self.logger.info('Received STOP event on HttpBackend') + if self._io_loop: + self._io_loop.stop() + self._io_loop.close() + if self.server_proc: self.server_proc.terminate() self.server_proc.join(timeout=10) @@ -216,6 +225,8 @@ class HttpBackend(Backend): self._service_registry_thread.join(timeout=5) self._service_registry_thread = None + self._io_loop = None + def notify_web_clients(self, event): """Notify all the connected web clients (over websocket) of a new event""" get_redis().publish(events_redis_topic, str(event)) @@ -248,14 +259,23 @@ class HttpBackend(Backend): application.config['redis_queue'] = self.bus.redis_queue application.secret_key = self._get_secret_key() - kwargs = { - 'bind': f'{self.bind_address}:{self.port}', - 'workers': (cpu_count() * 2) + 1, - 'worker_class_str': f'{__package__}.app.UvicornWorker', - 'timeout': 30, - } - WSGIApplicationWrapper(f'{__package__}.app:application', kwargs).run() + container = WSGIContainer(application) + server = Application( + [ + (r'/ws/events', WSEventProxy), + (r'.*', FallbackHandler, {'fallback': container}), + ] + ) + + server.listen(address=self.bind_address, port=self.port) + self._io_loop = IOLoop.instance() + + try: + self._io_loop.start() + except Exception as e: + if not self.should_stop(): + raise e return proc diff --git a/platypush/backend/http/app/routes/websocket.py b/platypush/backend/http/app/routes/websocket.py deleted file mode 100644 index d0bc4be3..00000000 --- a/platypush/backend/http/app/routes/websocket.py +++ /dev/null @@ -1,65 +0,0 @@ -from logging import getLogger - -from flask import Blueprint, request -from simple_websocket import ConnectionClosed, Server - -from platypush.backend.http.app import template_folder -from platypush.backend.http.app.utils import authenticate -from platypush.backend.http.ws import events_redis_topic -from platypush.message.event import Event -from platypush.utils import get_redis - - -ws = Blueprint('ws', __name__, template_folder=template_folder) - -__routes__ = [ws] - -logger = getLogger(__name__) - - -@ws.route('/ws/events', websocket=True) -@authenticate(json=True) -def ws_events_route(): - """ - A websocket endpoint to asynchronously receive events generated from the - application. - - This endpoint is mainly used by web clients to listen for the events - generated by the application. - """ - - sock = Server(request.environ, ping_interval=25) - ws_key = (sock.environ['REMOTE_ADDR'], int(sock.environ['REMOTE_PORT'])) - sub = get_redis().pubsub() - sub.subscribe(events_redis_topic) - logger.info('Started websocket connection with %s', ws_key) - - try: - for msg in sub.listen(): - if ( - msg.get('type') != 'message' - and msg.get('channel').decode() != events_redis_topic - ): - continue - - try: - evt = Event.build(msg.get('data').decode()) - except Exception as e: - logger.warning('Error parsing event: %s: %s', msg.get('data'), e) - continue - - sock.send(str(evt)) - except ConnectionClosed as e: - logger.info( - 'Websocket connection to %s closed, reason=%s, message=%s', - ws_key, - e.reason, - e.message, - ) - finally: - sub.unsubscribe(events_redis_topic) - - return '' - - -# vim:sw=4:ts=4:et: diff --git a/platypush/backend/http/ws.py b/platypush/backend/http/ws.py index bac62a87..2dcbee8a 100644 --- a/platypush/backend/http/ws.py +++ b/platypush/backend/http/ws.py @@ -1,3 +1,75 @@ -from platypush.config import Config +from logging import getLogger +from threading import Thread +from typing_extensions import override -events_redis_topic = f'__platypush/{Config.get("device_id")}/events' # type: ignore +from redis import ConnectionError +from tornado.ioloop import IOLoop +from tornado.websocket import WebSocketHandler + +from platypush.config import Config +from platypush.message.event import Event +from platypush.utils import get_redis + +events_redis_topic = f'_platypush/{Config.get("device_id")}/events' # type: ignore +logger = getLogger(__name__) + + +class WSEventProxy(WebSocketHandler, Thread): + """ + Websocket event proxy mapped to ``/ws/events``. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._sub = get_redis().pubsub() + self._io_loop = IOLoop.current() + + @override + def open(self, *_, **__): + logger.info('Started websocket connection with %s', self.request.remote_ip) + self.name = f'ws:events@{self.request.remote_ip}' + self.start() + + @override + def on_message(self, *_, **__): + pass + + @override + def data_received(self, *_, **__): + pass + + @override + def run(self) -> None: + super().run() + self._sub.subscribe(events_redis_topic) + + try: + for msg in self._sub.listen(): + if ( + msg.get('type') != 'message' + and msg.get('channel').decode() != events_redis_topic + ): + continue + + try: + evt = Event.build(msg.get('data').decode()) + except Exception as e: + logger.warning('Error parsing event: %s: %s', msg.get('data'), e) + continue + + self._io_loop.asyncio_loop.call_soon_threadsafe( # type: ignore + self.write_message, str(evt) + ) + except ConnectionError: + pass + + @override + def on_close(self): + self._sub.unsubscribe(events_redis_topic) + self._sub.close() + logger.info( + 'Websocket connection to %s closed, reason=%s, message=%s', + self.request.remote_ip, + self.close_code, + self.close_reason, + ) diff --git a/platypush/backend/http/wsgi/__init__.py b/platypush/backend/http/wsgi/__init__.py deleted file mode 100644 index a2798e05..00000000 --- a/platypush/backend/http/wsgi/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any, Dict - -from gunicorn.app.wsgiapp import WSGIApplication -from uvicorn.workers import UvicornWorker as BaseUvicornWorker - - -class UvicornWorker(BaseUvicornWorker): - CONFIG_KWARGS: Dict[str, Any] = {"loop": "auto", "http": "auto", "lifespan": "on"} - - -class WSGIApplicationWrapper(WSGIApplication): - """ - Wrapper for the Flask application into a WSGI application. - """ - - def __init__(self, app_uri, options=None): - self.options = options or {} - self.app_uri = app_uri - super().__init__() - - def load_config(self): - config = { - key: value - for key, value in self.options.items() - if key in self.cfg.settings and value is not None # type: ignore - } - for key, value in config.items(): - self.cfg.set(key.lower(), value) # type: ignore diff --git a/platypush/backend/nfc/__init__.py b/platypush/backend/nfc/__init__.py index fe1ca3d4..c481b77a 100644 --- a/platypush/backend/nfc/__init__.py +++ b/platypush/backend/nfc/__init__.py @@ -2,8 +2,12 @@ import base64 import json from platypush.backend import Backend -from platypush.message.event.nfc import NFCTagDetectedEvent, NFCTagRemovedEvent, NFCDeviceConnectedEvent, \ - NFCDeviceDisconnectedEvent +from platypush.message.event.nfc import ( + NFCTagDetectedEvent, + NFCTagRemovedEvent, + NFCDeviceConnectedEvent, + NFCDeviceDisconnectedEvent, +) class NfcBackend(Backend): @@ -20,7 +24,7 @@ class NfcBackend(Backend): Requires: * **nfcpy** >= 1.0 (``pip install 'nfcpy>=1.0'``) - * **ndef** (``pip install ndef``) + * **ndef** (``pip install ndeflib``) Run the following to check if your device is compatible with nfcpy and the right permissions are set:: @@ -49,7 +53,11 @@ class NfcBackend(Backend): self._clf = nfc.ContactlessFrontend() self._clf.open(self.device_id) self.bus.post(NFCDeviceConnectedEvent(reader=self._get_device_str())) - self.logger.info('Initialized NFC reader backend on device {}'.format(self._get_device_str())) + self.logger.info( + 'Initialized NFC reader backend on device {}'.format( + self._get_device_str() + ) + ) return self._clf @@ -107,51 +115,92 @@ class NfcBackend(Backend): r = { **r, 'type': 'smartposter', - **{attr: getattr(record, attr) for attr in ['resource', 'titles', 'title', 'action', 'icon', - 'icons', 'resource_size', 'resource_type']}, + **{ + attr: getattr(record, attr) + for attr in [ + 'resource', + 'titles', + 'title', + 'action', + 'icon', + 'icons', + 'resource_size', + 'resource_type', + ] + }, } elif isinstance(record, DeviceInformationRecord): r = { **r, 'type': 'device_info', - **{attr: getattr(record, attr) for attr in ['vendor_name', 'model_name', 'unique_name', - 'uuid_string', 'version_string']}, + **{ + attr: getattr(record, attr) + for attr in [ + 'vendor_name', + 'model_name', + 'unique_name', + 'uuid_string', + 'version_string', + ] + }, } elif isinstance(record, WifiSimpleConfigRecord): r = { **r, 'type': 'wifi_simple_config', - **{attr: record[attr] for attr in record.attribute_names()} + **{attr: record[attr] for attr in record.attribute_names()}, } elif isinstance(record, WifiPeerToPeerRecord): r = { **r, 'type': 'wifi_peer_to_peer', - **{attr: record[attr] for attr in record.attribute_names()} + **{attr: record[attr] for attr in record.attribute_names()}, } elif isinstance(record, BluetoothEasyPairingRecord): r = { **r, 'type': 'bluetooth_easy_pairing', - **{attr: getattr(record, attr) for attr in ['device_address', 'device_name', 'device_class']}, + **{ + attr: getattr(record, attr) + for attr in ['device_address', 'device_name', 'device_class'] + }, } elif isinstance(record, BluetoothLowEnergyRecord): r = { **r, 'type': 'bluetooth_low_energy', - **{attr: getattr(record, attr) for attr in ['device_address', 'device_name', 'role_capabilities', - 'appearance', 'flags', 'security_manager_tk_value', - 'secure_connections_confirmation_value', - 'secure_connections_random_value']}, + **{ + attr: getattr(record, attr) + for attr in [ + 'device_address', + 'device_name', + 'role_capabilities', + 'appearance', + 'flags', + 'security_manager_tk_value', + 'secure_connections_confirmation_value', + 'secure_connections_random_value', + ] + }, } elif isinstance(record, SignatureRecord): r = { **r, 'type': 'signature', - **{attr: getattr(record, attr) for attr in ['version', 'signature_type', 'hash_type', 'signature', - 'signature_uri', 'certificate_format', - 'certificate_store', 'certificate_uri', - 'secure_connections_random_value']}, + **{ + attr: getattr(record, attr) + for attr in [ + 'version', + 'signature_type', + 'hash_type', + 'signature', + 'signature_uri', + 'certificate_format', + 'certificate_store', + 'certificate_uri', + 'secure_connections_random_value', + ] + }, } else: r = { @@ -175,7 +224,11 @@ class NfcBackend(Backend): tag_id = self._parse_id(tag) records = self._parse_records(tag) - self.bus.post(NFCTagDetectedEvent(reader=self._get_device_str(), tag_id=tag_id, records=records)) + self.bus.post( + NFCTagDetectedEvent( + reader=self._get_device_str(), tag_id=tag_id, records=records + ) + ) return True return callback @@ -183,7 +236,9 @@ class NfcBackend(Backend): def _on_release(self): def callback(tag): tag_id = self._parse_id(tag) - self.bus.post(NFCTagRemovedEvent(reader=self._get_device_str(), tag_id=tag_id)) + self.bus.post( + NFCTagRemovedEvent(reader=self._get_device_str(), tag_id=tag_id) + ) return callback @@ -193,10 +248,12 @@ class NfcBackend(Backend): while not self.should_stop(): try: clf = self._get_clf() - clf.connect(rdwr={ - 'on-connect': self._on_connect(), - 'on-release': self._on_release(), - }) + clf.connect( + rdwr={ + 'on-connect': self._on_connect(), + 'on-release': self._on_release(), + } + ) finally: self.close() diff --git a/platypush/backend/nfc/manifest.yaml b/platypush/backend/nfc/manifest.yaml index 90f8ef5b..f8259dfc 100644 --- a/platypush/backend/nfc/manifest.yaml +++ b/platypush/backend/nfc/manifest.yaml @@ -8,6 +8,7 @@ manifest: platypush.message.event.nfc.NFCTagRemovedEvent: when an NFC tag is removed install: pip: - - ndef + - nfcpy>=1.0 + - ndeflib package: platypush.backend.nfc type: backend diff --git a/platypush/backend/weather/buienradar/manifest.yaml b/platypush/backend/weather/buienradar/manifest.yaml index 12df0118..130a4201 100644 --- a/platypush/backend/weather/buienradar/manifest.yaml +++ b/platypush/backend/weather/buienradar/manifest.yaml @@ -3,6 +3,7 @@ manifest: platypush.message.event.weather.NewWeatherConditionEvent: when there is a weather condition update install: - pip: [] + pip: + - buienradar package: platypush.backend.weather.buienradar type: backend diff --git a/platypush/plugins/assistant/google/pushtotalk/__init__.py b/platypush/plugins/assistant/google/pushtotalk/__init__.py index 13afb59c..d1a511ca 100644 --- a/platypush/plugins/assistant/google/pushtotalk/__init__.py +++ b/platypush/plugins/assistant/google/pushtotalk/__init__.py @@ -7,9 +7,13 @@ import os from typing import Optional, Dict, Any from platypush.context import get_bus, get_plugin -from platypush.message.event.assistant import ConversationStartEvent, \ - ConversationEndEvent, SpeechRecognizedEvent, VolumeChangedEvent, \ - ResponseEvent +from platypush.message.event.assistant import ( + ConversationStartEvent, + ConversationEndEvent, + SpeechRecognizedEvent, + VolumeChangedEvent, + ResponseEvent, +) from platypush.message.event.google import GoogleDeviceOnOffEvent @@ -34,29 +38,42 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): * **tenacity** (``pip install tenacity``) * **google-assistant-sdk** (``pip install google-assistant-sdk[samples]``) + * **google-auth** (``pip install google-auth``) """ api_endpoint = 'embeddedassistant.googleapis.com' grpc_deadline = 60 * 3 + 5 device_handler = None + _default_credentials_file = os.path.join( + os.path.expanduser('~'), + '.config', + 'google-oauthlib-tool', + 'credentials.json', + ) - def __init__(self, - credentials_file=os.path.join( - os.path.expanduser('~'), '.config', - 'google-oauthlib-tool', 'credentials.json'), - device_config=os.path.join( - os.path.expanduser('~'), '.config', 'googlesamples-assistant', - 'device_config.json'), - language='en-US', - play_response=True, - tts_plugin=None, - tts_args=None, - **kwargs): + _default_device_config = os.path.join( + os.path.expanduser('~'), + '.config', + 'googlesamples-assistant', + 'device_config.json', + ) + + def __init__( + self, + credentials_file=_default_credentials_file, + device_config=_default_device_config, + language='en-US', + play_response=True, + tts_plugin=None, + tts_args=None, + **kwargs + ): """ :param credentials_file: Path to the Google OAuth credentials file (default: ~/.config/google-oauthlib-tool/credentials.json). - See https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials + See + https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials for instructions to get your own credentials file. :type credentials_file: str @@ -81,6 +98,7 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): """ import googlesamples.assistant.grpc.audio_helpers as audio_helpers + super().__init__(**kwargs) self.audio_sample_rate = audio_helpers.DEFAULT_AUDIO_SAMPLE_RATE @@ -114,8 +132,9 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): self.credentials.refresh(self.http_request) except Exception as ex: self.logger.error('Error loading credentials: %s', str(ex)) - self.logger.error('Run google-oauthlib-tool to initialize ' - 'new OAuth 2.0 credentials.') + self.logger.error( + 'Run google-oauthlib-tool to initialize ' 'new OAuth 2.0 credentials.' + ) raise self.grpc_channel = None @@ -128,27 +147,25 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): self.interactions = [] # Create an authorized gRPC channel. - self.grpc_channel = secure_authorized_channel(self.credentials, self.http_request, self.api_endpoint) + self.grpc_channel = secure_authorized_channel( + self.credentials, self.http_request, self.api_endpoint + ) self.logger.info('Connecting to {}'.format(self.api_endpoint)) # Configure audio source and sink. audio_device = None - audio_source = audio_device = ( - audio_device or audio_helpers.SoundDeviceStream( - sample_rate=self.audio_sample_rate, - sample_width=self.audio_sample_width, - block_size=self.audio_block_size, - flush_size=self.audio_flush_size - ) + audio_source = audio_device = audio_device or audio_helpers.SoundDeviceStream( + sample_rate=self.audio_sample_rate, + sample_width=self.audio_sample_width, + block_size=self.audio_block_size, + flush_size=self.audio_flush_size, ) - audio_sink = ( - audio_device or audio_helpers.SoundDeviceStream( - sample_rate=self.audio_sample_rate, - sample_width=self.audio_sample_width, - block_size=self.audio_block_size, - flush_size=self.audio_flush_size - ) + audio_sink = audio_device or audio_helpers.SoundDeviceStream( + sample_rate=self.audio_sample_rate, + sample_width=self.audio_sample_width, + block_size=self.audio_block_size, + flush_size=self.audio_flush_size, ) # Create conversation stream with the given audio source and sink. @@ -162,21 +179,28 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): self._install_device_handlers() def on_conversation_start(self): - """ Conversation start handler """ + """Conversation start handler""" + def handler(): get_bus().post(ConversationStartEvent(assistant=self)) return handler def on_conversation_end(self): - """ Conversation end handler """ + """Conversation end handler""" + def handler(with_follow_on_turn): - get_bus().post(ConversationEndEvent(assistant=self, with_follow_on_turn=with_follow_on_turn)) + get_bus().post( + ConversationEndEvent( + assistant=self, with_follow_on_turn=with_follow_on_turn + ) + ) return handler def on_speech_recognized(self): - """ Speech recognized handler """ + """Speech recognized handler""" + def handler(phrase): get_bus().post(SpeechRecognizedEvent(assistant=self, phrase=phrase)) self.interactions.append({'request': phrase}) @@ -184,14 +208,16 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): return handler def on_volume_changed(self): - """ Volume changed event """ + """Volume changed event""" + def handler(volume): get_bus().post(VolumeChangedEvent(assistant=self, volume=volume)) return handler def on_response(self): - """ Response handler """ + """Response handler""" + def handler(response): get_bus().post(ResponseEvent(assistant=self, response_text=response)) @@ -207,8 +233,14 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): return handler @action - def start_conversation(self, *args, language: Optional[str] = None, tts_plugin: Optional[str] = None, - tts_args: Optional[Dict[str, Any]] = None, **kwargs): + def start_conversation( + self, + *_, + language: Optional[str] = None, + tts_plugin: Optional[str] = None, + tts_args: Optional[Dict[str, Any]] = None, + **__ + ): """ Start a conversation @@ -242,27 +274,31 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): self._init_assistant() self.on_conversation_start() - with SampleAssistant(language_code=language, - device_model_id=self.device_model_id, - device_id=self.device_id, - conversation_stream=self.conversation_stream, - display=None, - channel=self.grpc_channel, - deadline_sec=self.grpc_deadline, - play_response=play_response, - device_handler=self.device_handler, - on_conversation_start=self.on_conversation_start(), - on_conversation_end=self.on_conversation_end(), - on_volume_changed=self.on_volume_changed(), - on_response=self.on_response(), - on_speech_recognized=self.on_speech_recognized()) as self.assistant: + with SampleAssistant( + language_code=language, + device_model_id=self.device_model_id, + device_id=self.device_id, + conversation_stream=self.conversation_stream, + display=None, + channel=self.grpc_channel, + deadline_sec=self.grpc_deadline, + play_response=play_response, + device_handler=self.device_handler, + on_conversation_start=self.on_conversation_start(), + on_conversation_end=self.on_conversation_end(), + on_volume_changed=self.on_volume_changed(), + on_response=self.on_response(), + on_speech_recognized=self.on_speech_recognized(), + ) as self.assistant: continue_conversation = True while continue_conversation: try: continue_conversation = self.assistant.assist() except Exception as e: - self.logger.warning('Unhandled assistant exception: {}'.format(str(e))) + self.logger.warning( + 'Unhandled assistant exception: {}'.format(str(e)) + ) self.logger.exception(e) self._init_assistant() @@ -297,14 +333,18 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): def _install_device_handlers(self): import googlesamples.assistant.grpc.device_helpers as device_helpers + self.device_handler = device_helpers.DeviceRequestHandler(self.device_id) @self.device_handler.command('action.devices.commands.OnOff') - def handler(on): - get_bus().post(GoogleDeviceOnOffEvent( - device_id=self.device_id, - device_model_id=self.device_model_id, - on=on)) + def handler(on): # type: ignore + get_bus().post( + GoogleDeviceOnOffEvent( + device_id=self.device_id, + device_model_id=self.device_model_id, + on=on, + ) + ) # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/assistant/google/pushtotalk/manifest.yaml b/platypush/plugins/assistant/google/pushtotalk/manifest.yaml index 2f374507..efcd1a89 100644 --- a/platypush/plugins/assistant/google/pushtotalk/manifest.yaml +++ b/platypush/plugins/assistant/google/pushtotalk/manifest.yaml @@ -10,5 +10,6 @@ manifest: pip: - tenacity - google-assistant-sdk + - google-auth package: platypush.plugins.assistant.google.pushtotalk type: plugin diff --git a/platypush/plugins/camera/gstreamer/__init__.py b/platypush/plugins/camera/gstreamer/__init__.py index 14a1d5fe..bb745b3c 100644 --- a/platypush/plugins/camera/gstreamer/__init__.py +++ b/platypush/plugins/camera/gstreamer/__init__.py @@ -15,6 +15,7 @@ class CameraGstreamerPlugin(CameraPlugin): Requires: * **gst-python** + * **pygobject** On Debian and derived systems: @@ -39,15 +40,24 @@ class CameraGstreamerPlugin(CameraPlugin): pipeline = Pipeline() src = pipeline.add_source('v4l2src', device=camera.info.device) convert = pipeline.add('videoconvert') + assert camera.info and camera.info.resolution + video_filter = pipeline.add( - 'capsfilter', caps='video/x-raw,format=RGB,width={width},height={height},framerate={fps}/1'.format( - width=camera.info.resolution[0], height=camera.info.resolution[1], fps=camera.info.fps)) + 'capsfilter', + caps='video/x-raw,format=RGB,width={width},height={height},framerate={fps}/1'.format( + width=camera.info.resolution[0], + height=camera.info.resolution[1], + fps=camera.info.fps, + ), + ) sink = pipeline.add_sink('appsink', name='appsink', sync=False) pipeline.link(src, convert, video_filter, sink) return pipeline - def start_camera(self, camera: GStreamerCamera, preview: bool = False, *args, **kwargs): + def start_camera( + self, camera: GStreamerCamera, preview: bool = False, *args, **kwargs + ): super().start_camera(*args, camera=camera, preview=preview, **kwargs) if camera.object: camera.object.play() @@ -56,16 +66,27 @@ class CameraGstreamerPlugin(CameraPlugin): if camera.object: camera.object.stop() - def capture_frame(self, camera: GStreamerCamera, *args, **kwargs) -> Optional[ImageType]: - timed_out = not camera.object.data_ready.wait(timeout=5 + (1. / camera.info.fps)) + def capture_frame(self, camera: GStreamerCamera, *_, **__) -> Optional[ImageType]: + if not (camera.info and camera.info.fps and camera.info.resolution): + return None + + timed_out = not camera.object.data_ready.wait( + timeout=5 + (1.0 / camera.info.fps) + ) if timed_out: self.logger.warning('Frame capture timeout') - return + return None data = camera.object.data + if data is None: + return None + camera.object.data_ready.clear() - if not data and len(data) != camera.info.resolution[0] * camera.info.resolution[1] * 3: - return + if ( + not data + and len(data) != camera.info.resolution[0] * camera.info.resolution[1] * 3 + ): + return None return Image.frombytes('RGB', camera.info.resolution, data) diff --git a/platypush/plugins/camera/gstreamer/manifest.yaml b/platypush/plugins/camera/gstreamer/manifest.yaml index 9eec1699..43e2cae4 100644 --- a/platypush/plugins/camera/gstreamer/manifest.yaml +++ b/platypush/plugins/camera/gstreamer/manifest.yaml @@ -4,6 +4,7 @@ manifest: pip: - numpy - Pillow + - pygobject apt: - python3-gi - python3-gst-1.0 diff --git a/platypush/plugins/google/calendar/manifest.yaml b/platypush/plugins/google/calendar/manifest.yaml index f73f0f58..c1b373d4 100644 --- a/platypush/plugins/google/calendar/manifest.yaml +++ b/platypush/plugins/google/calendar/manifest.yaml @@ -3,6 +3,7 @@ manifest: install: pip: - google-api-python-client + - google-auth - oauth2client package: platypush.plugins.google.calendar type: plugin diff --git a/platypush/plugins/google/drive/manifest.yaml b/platypush/plugins/google/drive/manifest.yaml index 13fc150f..6114d3a2 100644 --- a/platypush/plugins/google/drive/manifest.yaml +++ b/platypush/plugins/google/drive/manifest.yaml @@ -3,6 +3,7 @@ manifest: install: pip: - google-api-python-client + - google-auth - oauth2client package: platypush.plugins.google.drive type: plugin diff --git a/platypush/plugins/google/fit/manifest.yaml b/platypush/plugins/google/fit/manifest.yaml index abd723ff..390b8172 100644 --- a/platypush/plugins/google/fit/manifest.yaml +++ b/platypush/plugins/google/fit/manifest.yaml @@ -3,6 +3,7 @@ manifest: install: pip: - google-api-python-client + - google-auth - oauth2client package: platypush.plugins.google.fit type: plugin diff --git a/platypush/plugins/google/mail/manifest.yaml b/platypush/plugins/google/mail/manifest.yaml index fe047290..1a7df968 100644 --- a/platypush/plugins/google/mail/manifest.yaml +++ b/platypush/plugins/google/mail/manifest.yaml @@ -3,6 +3,7 @@ manifest: install: pip: - google-api-python-client + - google-auth - oauth2client package: platypush.plugins.google.mail type: plugin diff --git a/platypush/plugins/google/maps/manifest.yaml b/platypush/plugins/google/maps/manifest.yaml index 227a36ec..2af250c2 100644 --- a/platypush/plugins/google/maps/manifest.yaml +++ b/platypush/plugins/google/maps/manifest.yaml @@ -3,6 +3,7 @@ manifest: install: pip: - google-api-python-client + - google-auth - oauth2client package: platypush.plugins.google.maps type: plugin diff --git a/platypush/plugins/google/pubsub/manifest.yaml b/platypush/plugins/google/pubsub/manifest.yaml index 569b3c9c..f546ba3c 100644 --- a/platypush/plugins/google/pubsub/manifest.yaml +++ b/platypush/plugins/google/pubsub/manifest.yaml @@ -3,6 +3,7 @@ manifest: install: pip: - google-api-python-client + - google-auth - oauth2client - google-cloud-pubsub package: platypush.plugins.google.pubsub diff --git a/platypush/plugins/google/translate/manifest.yaml b/platypush/plugins/google/translate/manifest.yaml index 561878ca..bee4f43d 100644 --- a/platypush/plugins/google/translate/manifest.yaml +++ b/platypush/plugins/google/translate/manifest.yaml @@ -3,6 +3,7 @@ manifest: install: pip: - google-api-python-client + - google-auth - oauth2client - google-cloud-translate package: platypush.plugins.google.translate diff --git a/platypush/plugins/google/youtube/manifest.yaml b/platypush/plugins/google/youtube/manifest.yaml index a08ca8f5..94ee6d67 100644 --- a/platypush/plugins/google/youtube/manifest.yaml +++ b/platypush/plugins/google/youtube/manifest.yaml @@ -3,6 +3,7 @@ manifest: install: pip: - google-api-python-client + - google-auth - oauth2client package: platypush.plugins.google.youtube type: plugin diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py index a2abe096..67949857 100644 --- a/platypush/plugins/media/__init__.py +++ b/platypush/plugins/media/__init__.py @@ -30,7 +30,7 @@ class MediaPlugin(Plugin, ABC): Requires: * A media player installed (supported so far: mplayer, vlc, mpv, omxplayer, chromecast) - * **python-libtorrent-bin** (``pip install python-libtorrent-bin``), optional, for torrent support over native + * **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support over native library * *rtorrent* installed - optional, for torrent support over rtorrent * **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support @@ -43,41 +43,123 @@ class MediaPlugin(Plugin, ABC): # A media plugin can either be local or remote (e.g. control media on # another device) _is_local = True - _NOT_IMPLEMENTED_ERR = NotImplementedError('This method must be implemented in a derived class') + _NOT_IMPLEMENTED_ERR = NotImplementedError( + 'This method must be implemented in a derived class' + ) # Supported audio extensions audio_extensions = { - '3gp', 'aa', 'aac', 'aax', 'act', 'aiff', 'amr', 'ape', 'au', - 'awb', 'dct', 'dss', 'dvf', 'flac', 'gsm', 'iklax', 'ivs', - 'm4a', 'm4b', 'm4p', 'mmf', 'mp3', 'mpc', 'msv', 'nmf', 'nsf', - 'ogg,', 'opus', 'ra,', 'raw', 'sln', 'tta', 'vox', 'wav', - 'wma', 'wv', 'webm', '8svx', + '3gp', + 'aa', + 'aac', + 'aax', + 'act', + 'aiff', + 'amr', + 'ape', + 'au', + 'awb', + 'dct', + 'dss', + 'dvf', + 'flac', + 'gsm', + 'iklax', + 'ivs', + 'm4a', + 'm4b', + 'm4p', + 'mmf', + 'mp3', + 'mpc', + 'msv', + 'nmf', + 'nsf', + 'ogg,', + 'opus', + 'ra,', + 'raw', + 'sln', + 'tta', + 'vox', + 'wav', + 'wma', + 'wv', + 'webm', + '8svx', } # Supported video extensions video_extensions = { - 'webm', 'mkv', 'flv', 'flv', 'vob', 'ogv', 'ogg', 'drc', 'gif', - 'gifv', 'mng', 'avi', 'mts', 'm2ts', 'mov', 'qt', 'wmv', 'yuv', - 'rm', 'rmvb', 'asf', 'amv', 'mp4', 'm4p', 'm4v', 'mpg', 'mp2', - 'mpeg', 'mpe', 'mpv', 'mpg', 'mpeg', 'm2v', 'm4v', 'svi', - '3gp', '3g2', 'mxf', 'roq', 'nsv', 'flv', 'f4v', 'f4p', 'f4a', + 'webm', + 'mkv', + 'flv', + 'flv', + 'vob', + 'ogv', + 'ogg', + 'drc', + 'gif', + 'gifv', + 'mng', + 'avi', + 'mts', + 'm2ts', + 'mov', + 'qt', + 'wmv', + 'yuv', + 'rm', + 'rmvb', + 'asf', + 'amv', + 'mp4', + 'm4p', + 'm4v', + 'mpg', + 'mp2', + 'mpeg', + 'mpe', + 'mpv', + 'mpg', + 'mpeg', + 'm2v', + 'm4v', + 'svi', + '3gp', + '3g2', + 'mxf', + 'roq', + 'nsv', + 'flv', + 'f4v', + 'f4p', + 'f4a', 'f4b', } - _supported_media_plugins = {'media.mplayer', 'media.omxplayer', 'media.mpv', - 'media.vlc', 'media.chromecast', 'media.gstreamer'} + _supported_media_plugins = { + 'media.mplayer', + 'media.omxplayer', + 'media.mpv', + 'media.vlc', + 'media.chromecast', + 'media.gstreamer', + } _supported_media_types = ['file', 'jellyfin', 'plex', 'torrent', 'youtube'] _default_search_timeout = 60 # 60 seconds - def __init__(self, - media_dirs: Optional[List[str]] = None, - download_dir: Optional[str] = None, - env: Optional[Dict[str, str]] = None, - volume: Optional[Union[float, int]] = None, - torrent_plugin: str = 'torrent', - youtube_format: str = 'best', - *args, **kwargs): + def __init__( + self, + media_dirs: Optional[List[str]] = None, + download_dir: Optional[str] = None, + env: Optional[Dict[str, str]] = None, + volume: Optional[Union[float, int]] = None, + torrent_plugin: str = 'torrent', + youtube_format: str = 'best', + **kwargs, + ): """ :param media_dirs: Directories that will be scanned for media files when a search is performed (default: none) @@ -134,17 +216,21 @@ class MediaPlugin(Plugin, ABC): self._env = env or {} self.media_dirs = set( filter( - lambda _: os.path.isdir(_), - map( - lambda _: os.path.abspath(os.path.expanduser(_)), - media_dirs - ) + os.path.isdir, + [os.path.abspath(os.path.expanduser(d)) for d in media_dirs], ) ) - self.download_dir = os.path.abspath(os.path.expanduser( - download_dir or player_config.get('download_dir') or - os.path.join((os.path.expanduser('~') or self._env.get('HOME') or '/'), 'Downloads'))) + self.download_dir = os.path.abspath( + os.path.expanduser( + download_dir + or player_config.get('download_dir') + or os.path.join( + (os.path.expanduser('~') or self._env.get('HOME') or '/'), + 'Downloads', + ) + ) + ) if not os.path.isdir(self.download_dir): os.makedirs(self.download_dir, exist_ok=True) @@ -162,14 +248,17 @@ class MediaPlugin(Plugin, ABC): # More than 5% of the torrent has been downloaded if event.args.get('progress', 0) > 5 and event.args.get('files'): evt_queue.put(event.args['files']) + return handler @staticmethod def _is_youtube_resource(resource): - return resource.startswith('youtube:') \ - or resource.startswith('https://youtu.be/') \ - or resource.startswith('https://www.youtube.com/watch?v=') \ - or resource.startswith('https://youtube.com/watch?v=') + return ( + resource.startswith('youtube:') + or resource.startswith('https://youtu.be/') + or resource.startswith('https://www.youtube.com/watch?v=') + or resource.startswith('https://youtube.com/watch?v=') + ) def _get_resource(self, resource): """ @@ -194,15 +283,21 @@ class MediaPlugin(Plugin, ABC): resource = self.get_youtube_video_url(resource) elif resource.startswith('magnet:?'): - self.logger.info('Downloading torrent {} to {}'.format( - resource, self.download_dir)) + self.logger.info( + 'Downloading torrent {} to {}'.format(resource, self.download_dir) + ) torrents = get_plugin(self.torrent_plugin) evt_queue = queue.Queue() - torrents.download(resource, download_dir=self.download_dir, _async=True, is_media=True, - event_hndl=self._torrent_event_handler(evt_queue)) + torrents.download( + resource, + download_dir=self.download_dir, + _async=True, + is_media=True, + event_hndl=self._torrent_event_handler(evt_queue), + ) - resources = [f for f in evt_queue.get()] + resources = [f for f in evt_queue.get()] # noqa: C416 if resources: self._videos_queue = sorted(resources) @@ -265,7 +360,7 @@ class MediaPlugin(Plugin, ABC): @action def next(self): - """ Play the next item in the queue """ + """Play the next item in the queue""" self.stop() if self._videos_queue: @@ -323,8 +418,14 @@ class MediaPlugin(Plugin, ABC): raise self._NOT_IMPLEMENTED_ERR @action - def search(self, query, types=None, queue_results=False, autoplay=False, - search_timeout=_default_search_timeout): + def search( + self, + query, + types=None, + queue_results=False, + autoplay=False, + search_timeout=_default_search_timeout, + ): """ Perform a video search. @@ -356,8 +457,12 @@ class MediaPlugin(Plugin, ABC): results_queues[media_type] = queue.Queue() search_hndl = self._get_search_handler_by_type(media_type) worker_threads[media_type] = threading.Thread( - target=self._search_worker(query=query, search_hndl=search_hndl, - results_queue=results_queues[media_type])) + target=self._search_worker( + query=query, + search_hndl=search_hndl, + results_queue=results_queues[media_type], + ) + ) worker_threads[media_type].start() for media_type in types: @@ -368,11 +473,15 @@ class MediaPlugin(Plugin, ABC): results[media_type].extend(items) except queue.Empty: - self.logger.warning('Search for "{}" media type {} timed out'. - format(query, media_type)) + self.logger.warning( + 'Search for "{}" media type {} timed out'.format(query, media_type) + ) except Exception as e: - self.logger.warning('Error while searching for "{}", media type {}'. - format(query, media_type)) + self.logger.warning( + 'Error while searching for "{}", media type {}'.format( + query, media_type + ) + ) self.logger.exception(e) flattened_results = [] @@ -402,23 +511,29 @@ class MediaPlugin(Plugin, ABC): results_queue.put(search_hndl.search(query)) except Exception as e: results_queue.put(e) + return thread def _get_search_handler_by_type(self, search_type): if search_type == 'file': from .search import LocalMediaSearcher + return LocalMediaSearcher(self.media_dirs, media_plugin=self) if search_type == 'torrent': from .search import TorrentMediaSearcher + return TorrentMediaSearcher(media_plugin=self) if search_type == 'youtube': from .search import YoutubeMediaSearcher + return YoutubeMediaSearcher(media_plugin=self) if search_type == 'plex': from .search import PlexMediaSearcher + return PlexMediaSearcher(media_plugin=self) if search_type == 'jellyfin': from .search import JellyfinMediaSearcher + return JellyfinMediaSearcher(media_plugin=self) self.logger.warning('Unsupported search type: {}'.format(search_type)) @@ -463,18 +578,23 @@ class MediaPlugin(Plugin, ABC): http = get_backend('http') if not http: - self.logger.warning('Unable to stream {}: HTTP backend unavailable'. - format(media)) + self.logger.warning( + 'Unable to stream {}: HTTP backend unavailable'.format(media) + ) return self.logger.info('Starting streaming {}'.format(media)) - response = requests.put('{url}/media{download}'.format( - url=http.local_base_url, download='?download' if download else ''), - json={'source': media, 'subtitles': subtitles}) + response = requests.put( + '{url}/media{download}'.format( + url=http.local_base_url, download='?download' if download else '' + ), + json={'source': media, 'subtitles': subtitles}, + ) if not response.ok: - self.logger.warning('Unable to start streaming: {}'. - format(response.text or response.reason)) + self.logger.warning( + 'Unable to start streaming: {}'.format(response.text or response.reason) + ) return None, (response.text or response.reason) return response.json() @@ -483,16 +603,19 @@ class MediaPlugin(Plugin, ABC): def stop_streaming(self, media_id): http = get_backend('http') if not http: - self.logger.warning('Cannot unregister {}: HTTP backend unavailable'. - format(media_id)) + self.logger.warning( + 'Cannot unregister {}: HTTP backend unavailable'.format(media_id) + ) return - response = requests.delete('{url}/media/{id}'. - format(url=http.local_base_url, id=media_id)) + response = requests.delete( + '{url}/media/{id}'.format(url=http.local_base_url, id=media_id) + ) if not response.ok: - self.logger.warning('Unable to unregister media_id {}: {}'.format( - media_id, response.reason)) + self.logger.warning( + 'Unable to unregister media_id {}: {}'.format(media_id, response.reason) + ) return return response.json() @@ -511,11 +634,18 @@ class MediaPlugin(Plugin, ABC): @staticmethod def _youtube_search_html_parse(query): from .search import YoutubeMediaSearcher + # noinspection PyProtectedMember return YoutubeMediaSearcher()._youtube_search_html_parse(query) def get_youtube_video_url(self, url, youtube_format: Optional[str] = None): - ytdl_cmd = ['youtube-dl', '-f', youtube_format or self.youtube_format, '-g', url] + ytdl_cmd = [ + 'youtube-dl', + '-f', + youtube_format or self.youtube_format, + '-g', + url, + ] self.logger.info(f'Executing command {" ".join(ytdl_cmd)}') youtube_dl = subprocess.Popen(ytdl_cmd, stdout=subprocess.PIPE) url = youtube_dl.communicate()[0].decode().strip() @@ -564,15 +694,29 @@ class MediaPlugin(Plugin, ABC): if filename.startswith('file://'): filename = filename[7:] - result = subprocess.Popen(["ffprobe", filename], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + result = subprocess.Popen( + ["ffprobe", filename], stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) return functools.reduce( lambda t, t_i: t + t_i, - [float(t) * pow(60, i) for (i, t) in enumerate(re.search( - r'^Duration:\s*([^,]+)', [x.decode() - for x in result.stdout.readlines() - if "Duration" in x.decode()].pop().strip() - ).group(1).split(':')[::-1])] + [ + float(t) * pow(60, i) + for (i, t) in enumerate( + re.search( + r'^Duration:\s*([^,]+)', + [ + x.decode() + for x in result.stdout.readlines() + if "Duration" in x.decode() + ] + .pop() + .strip(), + ) + .group(1) + .split(':')[::-1] + ) + ], ) @action @@ -608,13 +752,14 @@ class MediaPlugin(Plugin, ABC): return if subtitles.startswith('file://'): - subtitles = subtitles[len('file://'):] + subtitles = subtitles[len('file://') :] if os.path.isfile(subtitles): return os.path.abspath(subtitles) else: content = requests.get(subtitles).content - f = tempfile.NamedTemporaryFile(prefix='media_subs_', - suffix='.srt', delete=False) + f = tempfile.NamedTemporaryFile( + prefix='media_subs_', suffix='.srt', delete=False + ) with f: f.write(content) diff --git a/platypush/plugins/media/gstreamer/__init__.py b/platypush/plugins/media/gstreamer/__init__.py index 557a65b7..44e6bbf5 100644 --- a/platypush/plugins/media/gstreamer/__init__.py +++ b/platypush/plugins/media/gstreamer/__init__.py @@ -15,6 +15,7 @@ class MediaGstreamerPlugin(MediaPlugin): Requires: * **gst-python** + * **pygobject** On Debian and derived systems: @@ -46,7 +47,7 @@ class MediaGstreamerPlugin(MediaPlugin): return pipeline @action - def play(self, resource: Optional[str] = None, **args): + def play(self, resource: Optional[str] = None, **_): """ Play a resource. @@ -67,20 +68,20 @@ class MediaGstreamerPlugin(MediaPlugin): pipeline = self._allocate_pipeline(resource) pipeline.play() if self.volume: - pipeline.set_volume(self.volume / 100.) + pipeline.set_volume(self.volume / 100.0) return self.status() @action def pause(self): - """ Toggle the paused state """ + """Toggle the paused state""" assert self._player, 'No instance is running' self._player.pause() return self.status() @action def quit(self): - """ Stop and quit the player (alias for :meth:`.stop`) """ + """Stop and quit the player (alias for :meth:`.stop`)""" self._stop_torrent() assert self._player, 'No instance is running' @@ -90,18 +91,18 @@ class MediaGstreamerPlugin(MediaPlugin): @action def stop(self): - """ Stop and quit the player (alias for :meth:`.quit`) """ + """Stop and quit the player (alias for :meth:`.quit`)""" return self.quit() @action def voldown(self, step=10.0): - """ Volume down by (default: 10)% """ + """Volume down by (default: 10)%""" # noinspection PyUnresolvedReferences return self.set_volume(self.get_volume().output - step) @action def volup(self, step=10.0): - """ Volume up by (default: 10)% """ + """Volume up by (default: 10)%""" # noinspection PyUnresolvedReferences return self.set_volume(self.get_volume().output + step) @@ -113,7 +114,7 @@ class MediaGstreamerPlugin(MediaPlugin): :return: Volume value between 0 and 100. """ assert self._player, 'No instance is running' - return self._player.get_volume() * 100. + return self._player.get_volume() * 100.0 @action def set_volume(self, volume): @@ -124,7 +125,7 @@ class MediaGstreamerPlugin(MediaPlugin): """ assert self._player, 'Player not running' # noinspection PyTypeChecker - volume = max(0, min(1, volume / 100.)) + volume = max(0, min(1, volume / 100.0)) self._player.set_volume(volume) MediaPipeline.post_event(MediaVolumeChangedEvent, volume=volume * 100) return self.status() @@ -142,12 +143,12 @@ class MediaGstreamerPlugin(MediaPlugin): @action def back(self, offset=60.0): - """ Back by (default: 60) seconds """ + """Back by (default: 60) seconds""" return self.seek(-offset) @action def forward(self, offset=60.0): - """ Forward by (default: 60) seconds """ + """Forward by (default: 60) seconds""" return self.seek(offset) @action @@ -158,7 +159,7 @@ class MediaGstreamerPlugin(MediaPlugin): return self._player and self._player.is_playing() @action - def load(self, resource, **args): + def load(self, resource, **_): """ Load/queue a resource/video to the player (alias for :meth:`.play`). """ @@ -166,7 +167,7 @@ class MediaGstreamerPlugin(MediaPlugin): @action def mute(self): - """ Toggle mute state """ + """Toggle mute state""" assert self._player, 'No instance is running' muted = self._player.is_muted() if muted: @@ -201,11 +202,15 @@ class MediaGstreamerPlugin(MediaPlugin): return { 'duration': length, - 'filename': self._resource[7:] if self._resource.startswith('file://') else self._resource, + 'filename': self._resource[7:] + if self._resource.startswith('file://') + else self._resource, 'mute': self._player.is_muted(), 'name': self._resource, 'pause': self._player.is_paused(), - 'percent_pos': pos / length if pos is not None and length is not None and pos >= 0 and length > 0 else 0, + 'percent_pos': pos / length + if pos is not None and length is not None and pos >= 0 and length > 0 + else 0, 'position': pos, 'seekable': length is not None and length > 0, 'state': self._gst_to_player_state(self._player.get_state()).value, @@ -217,6 +222,7 @@ class MediaGstreamerPlugin(MediaPlugin): def _gst_to_player_state(state) -> PlayerState: # noinspection PyUnresolvedReferences,PyPackageRequirements from gi.repository import Gst + if state == Gst.State.READY: return PlayerState.STOP if state == Gst.State.PAUSED: @@ -225,13 +231,13 @@ class MediaGstreamerPlugin(MediaPlugin): return PlayerState.PLAY return PlayerState.IDLE - def toggle_subtitles(self, *args, **kwargs): + def toggle_subtitles(self, *_, **__): raise NotImplementedError - def set_subtitles(self, filename, *args, **kwargs): + def set_subtitles(self, *_, **__): raise NotImplementedError - def remove_subtitles(self, *args, **kwargs): + def remove_subtitles(self, *_, **__): raise NotImplementedError diff --git a/platypush/plugins/media/gstreamer/manifest.yaml b/platypush/plugins/media/gstreamer/manifest.yaml index bdc0c567..8e50d745 100644 --- a/platypush/plugins/media/gstreamer/manifest.yaml +++ b/platypush/plugins/media/gstreamer/manifest.yaml @@ -4,6 +4,8 @@ manifest: apt: - python3-gi - python3-gst-1.0 + pip: + - pygobject pacman: - gst-python - python-gobject diff --git a/platypush/plugins/printer/cups/manifest.yaml b/platypush/plugins/printer/cups/manifest.yaml index 1d832d6c..fa8e2c61 100644 --- a/platypush/plugins/printer/cups/manifest.yaml +++ b/platypush/plugins/printer/cups/manifest.yaml @@ -3,5 +3,7 @@ manifest: install: pip: - pycups + apt: + - libcups2-dev package: platypush.plugins.printer.cups type: plugin diff --git a/platypush/plugins/sensor/ltr559/__init__.py b/platypush/plugins/sensor/ltr559/__init__.py index 840ffed6..35befb6f 100644 --- a/platypush/plugins/sensor/ltr559/__init__.py +++ b/platypush/plugins/sensor/ltr559/__init__.py @@ -19,6 +19,7 @@ class SensorLtr559Plugin(SensorPlugin): Requires: * ``ltr559`` (``pip install ltr559``) + * ``smbus`` (``pip install smbus``) Triggers: diff --git a/platypush/plugins/sensor/ltr559/manifest.yaml b/platypush/plugins/sensor/ltr559/manifest.yaml index 6ec27a59..b2efad9c 100644 --- a/platypush/plugins/sensor/ltr559/manifest.yaml +++ b/platypush/plugins/sensor/ltr559/manifest.yaml @@ -6,5 +6,6 @@ manifest: install: pip: - ltr559 + - smbus package: platypush.plugins.sensor.ltr559 type: plugin diff --git a/platypush/plugins/torrent/__init__.py b/platypush/plugins/torrent/__init__.py index 682f9f3d..71370795 100644 --- a/platypush/plugins/torrent/__init__.py +++ b/platypush/plugins/torrent/__init__.py @@ -9,10 +9,17 @@ import time from platypush.context import get_bus from platypush.plugins import Plugin, action -from platypush.message.event.torrent import \ - TorrentDownloadStartEvent, TorrentDownloadedMetadataEvent, TorrentStateChangeEvent, \ - TorrentDownloadProgressEvent, TorrentDownloadCompletedEvent, TorrentDownloadStopEvent, \ - TorrentPausedEvent, TorrentResumedEvent, TorrentQueuedEvent +from platypush.message.event.torrent import ( + TorrentDownloadStartEvent, + TorrentDownloadedMetadataEvent, + TorrentStateChangeEvent, + TorrentDownloadProgressEvent, + TorrentDownloadCompletedEvent, + TorrentDownloadStopEvent, + TorrentPausedEvent, + TorrentResumedEvent, + TorrentQueuedEvent, +) class TorrentPlugin(Plugin): @@ -21,7 +28,7 @@ class TorrentPlugin(Plugin): Requires: - * **python-libtorrent-bin** (``pip install python-libtorrent-bin``) + * **python-libtorrent** (``pip install python-libtorrent``) """ @@ -39,8 +46,14 @@ class TorrentPlugin(Plugin): # noinspection HttpUrlsUsage default_popcorn_base_url = 'http://popcorn-time.ga' - def __init__(self, download_dir=None, torrent_ports=None, imdb_key=None, popcorn_base_url=default_popcorn_base_url, - **kwargs): + def __init__( + self, + download_dir=None, + torrent_ports=None, + imdb_key=None, + popcorn_base_url=default_popcorn_base_url, + **kwargs, + ): """ :param download_dir: Directory where the videos/torrents will be downloaded (default: none) :type download_dir: str @@ -66,7 +79,9 @@ class TorrentPlugin(Plugin): self.imdb_key = imdb_key self.imdb_urls = {} - self.torrent_ports = torrent_ports if torrent_ports else self.default_torrent_ports + self.torrent_ports = ( + torrent_ports if torrent_ports else self.default_torrent_ports + ) self.download_dir = None self._sessions = {} self._lt_session = None @@ -109,11 +124,16 @@ class TorrentPlugin(Plugin): # noinspection PyCallingNonCallable def worker(cat): if cat not in self.categories: - raise RuntimeError('Unsupported category {}. Supported category: {}'. - format(cat, self.categories.keys())) + raise RuntimeError( + 'Unsupported category {}. Supported category: {}'.format( + cat, self.categories.keys() + ) + ) self.logger.info('Searching {} torrents for "{}"'.format(cat, query)) - results.extend(self.categories[cat](query, language=language, *args, **kwargs)) + results.extend( + self.categories[cat](query, *args, language=language, **kwargs) + ) workers = [ threading.Thread(target=worker, kwargs={'cat': category}) @@ -151,11 +171,14 @@ class TorrentPlugin(Plugin): imdb_results = self._imdb_query(query, category) result_queues = [queue.Queue()] * len(imdb_results) workers = [ - threading.Thread(target=self._torrent_search_worker, kwargs={ - 'imdb_id': imdb_results[i]['id'], - 'category': category, - 'q': result_queues[i], - }) + threading.Thread( + target=self._torrent_search_worker, + kwargs={ + 'imdb_id': imdb_results[i]['id'], + 'category': category, + 'q': result_queues[i], + }, + ) for i in range(len(imdb_results)) ] @@ -179,85 +202,99 @@ class TorrentPlugin(Plugin): return results @staticmethod - def _results_to_movies_response(results: List[dict], language: Optional[str] = None): - return sorted([ - { - 'imdb_id': result.get('imdb_id'), - 'type': 'movies', - 'file': item.get('file'), - 'title': '{title} [movies][{language}][{quality}]'.format( - title=result.get('title'), language=lang, quality=quality), - 'duration': int(result.get('runtime'), 0), - 'year': int(result.get('year'), 0), - 'synopsis': result.get('synopsis'), - 'trailer': result.get('trailer'), - 'genres': result.get('genres', []), - 'images': result.get('images', []), - 'rating': result.get('rating', {}), - 'language': lang, - 'quality': quality, - 'size': item.get('size'), - 'provider': item.get('provider'), - 'seeds': item.get('seed'), - 'peers': item.get('peer'), - 'url': item.get('url'), - } - for result in results - for (lang, items) in (result.get('torrents', {}) or {}).items() - if not language or language == lang - for (quality, item) in items.items() - if quality != '0' - ], key=lambda item: item.get('seeds', 0), reverse=True) + def _results_to_movies_response( + results: List[dict], language: Optional[str] = None + ): + return sorted( + [ + { + 'imdb_id': result.get('imdb_id'), + 'type': 'movies', + 'file': item.get('file'), + 'title': '{title} [movies][{language}][{quality}]'.format( + title=result.get('title'), language=lang, quality=quality + ), + 'duration': int(result.get('runtime') or 0), + 'year': int(result.get('year') or 0), + 'synopsis': result.get('synopsis'), + 'trailer': result.get('trailer'), + 'genres': result.get('genres', []), + 'images': result.get('images', []), + 'rating': result.get('rating', {}), + 'language': lang, + 'quality': quality, + 'size': item.get('size'), + 'provider': item.get('provider'), + 'seeds': item.get('seed'), + 'peers': item.get('peer'), + 'url': item.get('url'), + } + for result in results + for (lang, items) in (result.get('torrents', {}) or {}).items() + if not language or language == lang + for (quality, item) in items.items() + if quality != '0' + ], + key=lambda item: item.get('seeds', 0), + reverse=True, + ) @staticmethod def _results_to_tv_response(results: List[dict]): - return sorted([ - { - 'imdb_id': result.get('imdb_id'), - 'tvdb_id': result.get('tvdb_id'), - 'type': 'tv', - 'file': item.get('file'), - 'series': result.get('title'), - 'title': '{series} [S{season:02d}E{episode:02d}] {title} [tv][{quality}]'.format( - series=result.get('title'), - season=episode.get('season'), - episode=episode.get('episode'), - title=episode.get('title'), - quality=quality), - 'duration': int(result.get('runtime'), 0), - 'year': int(result.get('year'), 0), - 'synopsis': result.get('synopsis'), - 'overview': episode.get('overview'), - 'season': episode.get('season'), - 'episode': episode.get('episode'), - 'num_seasons': result.get('num_seasons'), - 'country': result.get('country'), - 'network': result.get('network'), - 'status': result.get('status'), - 'genres': result.get('genres', []), - 'images': result.get('images', []), - 'rating': result.get('rating', {}), - 'quality': quality, - 'provider': item.get('provider'), - 'seeds': item.get('seeds'), - 'peers': item.get('peers'), - 'url': item.get('url'), - } - for result in results - for episode in result.get('episodes', []) - for quality, item in (episode.get('torrents', {}) or {}).items() - if quality != '0' - ], key=lambda item: '{series}.{quality}.{season:02d}.{episode:02d}'.format( - series=item.get('series'), quality=item.get('quality'), - season=item.get('season'), episode=item.get('episode'))) + return sorted( + [ + { + 'imdb_id': result.get('imdb_id'), + 'tvdb_id': result.get('tvdb_id'), + 'type': 'tv', + 'file': item.get('file'), + 'series': result.get('title'), + 'title': '{series} [S{season:02d}E{episode:02d}] {title} [tv][{quality}]'.format( + series=result.get('title'), + season=episode.get('season'), + episode=episode.get('episode'), + title=episode.get('title'), + quality=quality, + ), + 'duration': int(result.get('runtime') or 0), + 'year': int(result.get('year') or 0), + 'synopsis': result.get('synopsis'), + 'overview': episode.get('overview'), + 'season': episode.get('season'), + 'episode': episode.get('episode'), + 'num_seasons': result.get('num_seasons'), + 'country': result.get('country'), + 'network': result.get('network'), + 'status': result.get('status'), + 'genres': result.get('genres', []), + 'images': result.get('images', []), + 'rating': result.get('rating', {}), + 'quality': quality, + 'provider': item.get('provider'), + 'seeds': item.get('seeds'), + 'peers': item.get('peers'), + 'url': item.get('url'), + } + for result in results + for episode in result.get('episodes', []) + for quality, item in (episode.get('torrents', {}) or {}).items() + if quality != '0' + ], + key=lambda item: '{series}.{quality}.{season:02d}.{episode:02d}'.format( + series=item.get('series'), + quality=item.get('quality'), + season=item.get('season'), + episode=item.get('episode'), + ), + ) def search_movies(self, query, language=None): return self._results_to_movies_response( - self._search_torrents(query, 'movies'), language=language) + self._search_torrents(query, 'movies'), language=language + ) def search_tv(self, query, **_): - return self._results_to_tv_response( - self._search_torrents(query, 'tv')) + return self._results_to_tv_response(self._search_torrents(query, 'tv')) def _get_torrent_info(self, torrent, download_dir): import libtorrent as lt @@ -296,7 +333,9 @@ class TorrentPlugin(Plugin): else: torrent_file = os.path.abspath(os.path.expanduser(torrent)) if not os.path.isfile(torrent_file): - raise RuntimeError('{} is not a valid torrent file'.format(torrent_file)) + raise RuntimeError( + '{} is not a valid torrent file'.format(torrent_file) + ) if torrent_file: file_info = lt.torrent_info(torrent_file) @@ -330,7 +369,9 @@ class TorrentPlugin(Plugin): while not transfer.is_finished(): if torrent not in self.transfers: - self.logger.info('Torrent {} has been stopped and removed'.format(torrent)) + self.logger.info( + 'Torrent {} has been stopped and removed'.format(torrent) + ) self._fire_event(TorrentDownloadStopEvent(url=torrent), event_hndl) break @@ -339,14 +380,14 @@ class TorrentPlugin(Plugin): if torrent_file: self.torrent_state[torrent]['size'] = torrent_file.total_size() - files = [os.path.join( - download_dir, - torrent_file.files().file_path(i)) + files = [ + os.path.join(download_dir, torrent_file.files().file_path(i)) for i in range(0, torrent_file.files().num_files()) ] if is_media: from platypush.plugins.media import MediaPlugin + # noinspection PyProtectedMember files = [f for f in files if MediaPlugin.is_video_file(f)] @@ -354,7 +395,9 @@ class TorrentPlugin(Plugin): self.torrent_state[torrent]['name'] = status.name self.torrent_state[torrent]['num_peers'] = status.num_peers self.torrent_state[torrent]['paused'] = status.paused - self.torrent_state[torrent]['progress'] = round(100 * status.progress, 2) + self.torrent_state[torrent]['progress'] = round( + 100 * status.progress, 2 + ) self.torrent_state[torrent]['state'] = status.state.name self.torrent_state[torrent]['title'] = status.name self.torrent_state[torrent]['torrent'] = torrent @@ -363,30 +406,51 @@ class TorrentPlugin(Plugin): self.torrent_state[torrent]['files'] = files if transfer.has_metadata() and not metadata_downloaded: - self._fire_event(TorrentDownloadedMetadataEvent(**self.torrent_state[torrent]), event_hndl) + self._fire_event( + TorrentDownloadedMetadataEvent(**self.torrent_state[torrent]), + event_hndl, + ) metadata_downloaded = True if status.state == status.downloading and not download_started: - self._fire_event(TorrentDownloadStartEvent(**self.torrent_state[torrent]), event_hndl) + self._fire_event( + TorrentDownloadStartEvent(**self.torrent_state[torrent]), + event_hndl, + ) download_started = True if last_status and status.progress != last_status.progress: - self._fire_event(TorrentDownloadProgressEvent(**self.torrent_state[torrent]), event_hndl) + self._fire_event( + TorrentDownloadProgressEvent(**self.torrent_state[torrent]), + event_hndl, + ) if not last_status or status.state != last_status.state: - self._fire_event(TorrentStateChangeEvent(**self.torrent_state[torrent]), event_hndl) + self._fire_event( + TorrentStateChangeEvent(**self.torrent_state[torrent]), + event_hndl, + ) if last_status and status.paused != last_status.paused: if status.paused: - self._fire_event(TorrentPausedEvent(**self.torrent_state[torrent]), event_hndl) + self._fire_event( + TorrentPausedEvent(**self.torrent_state[torrent]), + event_hndl, + ) else: - self._fire_event(TorrentResumedEvent(**self.torrent_state[torrent]), event_hndl) + self._fire_event( + TorrentResumedEvent(**self.torrent_state[torrent]), + event_hndl, + ) last_status = status time.sleep(self._MONITOR_CHECK_INTERVAL) if transfer and transfer.is_finished(): - self._fire_event(TorrentDownloadCompletedEvent(**self.torrent_state[torrent]), event_hndl) + self._fire_event( + TorrentDownloadCompletedEvent(**self.torrent_state[torrent]), + event_hndl, + ) self.remove(torrent) return files @@ -398,12 +462,15 @@ class TorrentPlugin(Plugin): return self._lt_session import libtorrent as lt + # noinspection PyArgumentList self._lt_session = lt.session() return self._lt_session @action - def download(self, torrent, download_dir=None, _async=False, event_hndl=None, is_media=False): + def download( + self, torrent, download_dir=None, _async=False, event_hndl=None, is_media=False + ): """ Download a torrent. @@ -445,10 +512,14 @@ class TorrentPlugin(Plugin): download_dir = os.path.abspath(os.path.expanduser(download_dir)) os.makedirs(download_dir, exist_ok=True) - info, file_info, torrent_file, magnet = self._get_torrent_info(torrent, download_dir) + info, file_info, torrent_file, magnet = self._get_torrent_info( + torrent, download_dir + ) if torrent in self._sessions: - self.logger.info('A torrent session is already running for {}'.format(torrent)) + self.logger.info( + 'A torrent session is already running for {}'.format(torrent) + ) return self.torrent_state.get(torrent, {}) session = self._get_session() @@ -472,12 +543,22 @@ class TorrentPlugin(Plugin): 'title': transfer.status().name, 'trackers': info['trackers'], 'save_path': download_dir, + 'torrent_file': torrent_file, } self._fire_event(TorrentQueuedEvent(url=torrent), event_hndl) - self.logger.info('Downloading "{}" to "{}" from [{}]'.format(info['name'], download_dir, torrent)) - monitor_thread = self._torrent_monitor(torrent=torrent, transfer=transfer, download_dir=download_dir, - event_hndl=event_hndl, is_media=is_media) + self.logger.info( + 'Downloading "{}" to "{}" from [{}]'.format( + info['name'], download_dir, torrent + ) + ) + monitor_thread = self._torrent_monitor( + torrent=torrent, + transfer=transfer, + download_dir=download_dir, + event_hndl=event_hndl, + is_media=is_media, + ) if not _async: return monitor_thread() @@ -565,7 +646,7 @@ class TorrentPlugin(Plugin): @staticmethod def _generate_rand_filename(length=16): name = '' - for i in range(0, length): + for _ in range(0, length): name += hex(random.randint(0, 15))[2:].upper() return name + '.torrent' diff --git a/platypush/plugins/torrent/manifest.yaml b/platypush/plugins/torrent/manifest.yaml index 86adf245..bbddb7f2 100644 --- a/platypush/plugins/torrent/manifest.yaml +++ b/platypush/plugins/torrent/manifest.yaml @@ -2,6 +2,6 @@ manifest: events: {} install: pip: - - python-libtorrent-bin + - python-libtorrent package: platypush.plugins.torrent type: plugin diff --git a/requirements.txt b/requirements.txt index 16b74f21..a78946b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ bcrypt croniter flask frozendict -gunicorn marshmallow marshmallow_dataclass paho-mqtt @@ -18,10 +17,9 @@ pyyaml redis requests rsa -simple_websocket sqlalchemy +tornado tz -uvicorn websocket-client -wsproto +websockets zeroconf>=0.27.0 diff --git a/setup.py b/setup.py index 7f5712ac..48f65213 100755 --- a/setup.py +++ b/setup.py @@ -65,7 +65,6 @@ setup( 'croniter', 'flask', 'frozendict', - 'gunicorn', 'marshmallow', 'marshmallow_dataclass', 'python-dateutil', @@ -74,13 +73,12 @@ setup( 'redis', 'requests', 'rsa', - 'simple_websocket', 'sqlalchemy', + 'tornado', 'tz', - 'uvicorn', 'websocket-client', + 'websockets', 'wheel', - 'wsproto', 'zeroconf>=0.27.0', ], extras_require={ @@ -109,6 +107,7 @@ setup( 'google-tts': [ 'oauth2client', 'google-api-python-client', + 'google-auth', 'google-cloud-texttospeech', ], # Support for OMXPlayer plugin @@ -116,7 +115,7 @@ setup( # Support for YouTube 'youtube': ['youtube-dl'], # Support for torrents download - 'torrent': ['python-libtorrent-bin'], + 'torrent': ['python-libtorrent'], # Generic support for cameras 'camera': ['numpy', 'Pillow'], # Support for RaspberryPi camera @@ -124,10 +123,10 @@ setup( # Support for inotify file monitors 'inotify': ['inotify'], # Support for Google Assistant - 'google-assistant-legacy': ['google-assistant-library'], - 'google-assistant': ['google-assistant-sdk[samples]'], + 'google-assistant-legacy': ['google-assistant-library', 'google-auth'], + 'google-assistant': ['google-assistant-sdk[samples]', 'google-auth'], # Support for the Google APIs - 'google': ['oauth2client', 'google-api-python-client'], + 'google': ['oauth2client', 'google-auth', 'google-api-python-client'], # Support for Last.FM scrobbler plugin 'lastfm': ['pylast'], # Support for custom hotword detection @@ -171,7 +170,7 @@ setup( # Support for BME280 environment sensor 'bme280': ['pimoroni-bme280'], # Support for LTR559 light/proximity sensor - 'ltr559': ['ltr559'], + 'ltr559': ['ltr559', 'smbus'], # Support for VL53L1X laser ranger/distance sensor 'vl53l1x': ['smbus2', 'vl53l1x'], # Support for Dropbox integration @@ -212,9 +211,9 @@ setup( # Support for Trello integration 'trello': ['py-trello'], # Support for Google Pub/Sub - 'google-pubsub': ['google-cloud-pubsub'], + 'google-pubsub': ['google-cloud-pubsub', 'google-auth'], # Support for Google Translate - 'google-translate': ['google-cloud-translate'], + 'google-translate': ['google-cloud-translate', 'google-auth'], # Support for keyboard/mouse plugin 'inputs': ['pyuserinput'], # Support for Buienradar weather forecast