From 92b691041ebe4d6f86564e8281e1c12c51891d5e Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 27 Dec 2017 10:18:51 +0100 Subject: [PATCH] Added more general media control plugin, #10 --- platypush/context/__init__.py | 38 ++++++++++ platypush/message/request/__init__.py | 7 +- platypush/plugins/media/ctrl.py | 94 +++++++++++++++++++++++++ platypush/plugins/music/mpd/__init__.py | 15 +++- platypush/plugins/video/omxplayer.py | 79 +++++++++++++++++++++ platypush/plugins/video/torrentcast.py | 44 ++++++++++++ platypush/utils/__init__.py | 35 --------- setup.py | 3 +- 8 files changed, 273 insertions(+), 42 deletions(-) create mode 100644 platypush/plugins/media/ctrl.py create mode 100644 platypush/plugins/video/omxplayer.py create mode 100644 platypush/plugins/video/torrentcast.py diff --git a/platypush/context/__init__.py b/platypush/context/__init__.py index b4b385e7a..e0e3455b8 100644 --- a/platypush/context/__init__.py +++ b/platypush/context/__init__.py @@ -51,5 +51,43 @@ def get_backend(name): return backends[name] +def get_plugin(plugin_name, reload=False): + """ Registers a plugin instance by name if not registered already, or + returns the registered plugin instance""" + global plugins + + if plugin_name in plugins and not reload: + return plugins[plugin_name] + + try: + plugin = importlib.import_module('platypush.plugins.' + plugin_name) + except ModuleNotFoundError as e: + logging.warning('No such plugin: {}'.format(plugin_name)) + raise RuntimeError(e) + + # e.g. plugins.music.mpd main class: MusicMpdPlugin + cls_name = functools.reduce( + lambda a,b: a.title() + b.title(), + (plugin_name.title().split('.')) + ) + 'Plugin' + + plugin_conf = Config.get_plugins()[plugin_name] \ + if plugin_name in Config.get_plugins() else {} + + try: + plugin_class = getattr(plugin, cls_name) + plugin = plugin_class(**plugin_conf) + plugins[plugin_name] = plugin + except AttributeError as e: + logging.warning('No such class in {}: {}'.format(plugin_name, cls_name)) + raise RuntimeError(e) + + plugins[plugin_name] = plugin + return plugin + +def register_plugin(name, plugin, **kwargs): + """ Registers a plugin instance by name """ + global plugins + # vim:sw=4:ts=4:et: diff --git a/platypush/message/request/__init__.py b/platypush/message/request/__init__.py index 0526cbcc4..fb580e39b 100644 --- a/platypush/message/request/__init__.py +++ b/platypush/message/request/__init__.py @@ -5,9 +5,10 @@ import traceback from threading import Thread +from platypush.context import get_plugin from platypush.message import Message from platypush.message.response import Response -from platypush.utils import get_or_load_plugin, get_module_and_method_from_action +from platypush.utils import get_module_and_method_from_action class Request(Message): """ Request message class """ @@ -59,7 +60,7 @@ class Request(Message): def _thread_func(n_tries): (module_name, method_name) = get_module_and_method_from_action(self.action) - plugin = get_or_load_plugin(module_name) + plugin = get_plugin(module_name) try: # Run the action @@ -75,7 +76,7 @@ class Request(Message): logging.exception(e) if n_tries: logging.info('Reloading plugin {} and retrying'.format(module_name)) - get_or_load_plugin(module_name, reload=True) + get_plugin(module_name, reload=True) _thread_func(n_tries-1) return finally: diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py new file mode 100644 index 000000000..aca44009e --- /dev/null +++ b/platypush/plugins/media/ctrl.py @@ -0,0 +1,94 @@ +import re +import subprocess + +from omxplayer import OMXPlayer + +from platypush.context import get_plugin +from platypush.message.response import Response + +from .. import Plugin + +class MediaCtrlPlugin(Plugin): + """ + Wrapper plugin to control audio and video media. + Examples of supported URL types: + - file:///media/movies/Movie.mp4 [requires omxplayer Python support] + - youtube:video:poAk9XgK7Cs [requires omxplayer+youtube-dl] + - magnet:?torrent_magnet [requires torrentcast] + - spotify:track:track_id [leverages plugins.music.mpd] + """ + + def __init__(self, omxplayer_args=[], torrentcast_port=9090, *args, **kwargs): + self.omxplayer_args = omxplayer_args + self.torrentcast_port = torrentcast_port + self.url = None + self.plugin = None + + @classmethod + def _get_type_and_resource_by_url(cls, url): + # MPD/Mopidy media (TODO support more mopidy types) + m = re.match('^https://open.spotify.com/([^/]+)/(.*)', url) + if m: url = 'spotify:{}:{}'.format(m.group(1), m.group(2)) + if url.startswith('spotify:') \ + or url.startswith('tunein:') \ + or url.startswith('soundcloud:'): + return ('mpd', url) + + # YouTube video + m = re.match('youtube:video:(.*)', url) + if m: url = 'https://www.youtube.com/watch?v={}'.format(m.group(1)) + if url.startswith('https://www.youtube.com/watch?v='): + return ('youtube:video', url) + + # Local media + if url.startswith('file://'): + m = re.match('^file://(.*)', url) + return ('file', m.group(1)) + + # URL to a .torrent media or Magnet link + if url.startswith('magnet:') or url.endswith('.torrent'): + return ('torrent', url) + + raise RuntimeError('Unknown URL type: {}'.format(url)) + + + def play(self, url): + (type, resource) = self._get_type_and_resource_by_url(url) + response = Response(output='', errors = []) + + if type == 'mpd': + self.plugin = get_plugin('music.mpd') + elif type == 'youtube:video' or type == 'file': + self.plugin = get_plugin('video.omxplayer') + elif type == 'torrent': + self.plugin = get_plugin('video.torrentcast') + + self.url = resource + return self.plugin.play(resource) + + def pause(self): + if self.plugin: return self.plugin.pause() + + + def stop(self): + if self.plugin: return self.plugin.stop() + + + def voldown(self): + if self.plugin: return self.plugin.voldown() + + + def volup(self): + if self.plugin: return self.plugin.volup() + + + def back(self): + if self.plugin: return self.plugin.back() + + + def forward(self): + if self.plugin: return self.plugin.forward() + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/plugins/music/mpd/__init__.py b/platypush/plugins/music/mpd/__init__.py index 87429c0c6..5d5b600d9 100644 --- a/platypush/plugins/music/mpd/__init__.py +++ b/platypush/plugins/music/mpd/__init__.py @@ -22,7 +22,10 @@ class MusicMpdPlugin(MusicPlugin): getattr(self.client, method)(*args, **kwargs) return self.status() - def play(self): + def play(self, resource=None): + if resource: + self.clear() + self.add(resource) return self._exec('play') def pause(self): @@ -56,8 +59,8 @@ class MusicMpdPlugin(MusicPlugin): self.setvol(str(new_volume)) return self.status() - def add(self, content): - return self._exec('add', content) + def add(self, resource): + return self._exec('add', resource) def playlistadd(self, playlist): return self._exec('playlistadd', playlist) @@ -65,6 +68,12 @@ class MusicMpdPlugin(MusicPlugin): def clear(self): return self._exec('clear') + def forward(self): + return self._exec('seekcur', '+15') + + def back(self): + return self._exec('seekcur', '-15') + def status(self): return Response(output=self.client.status()) diff --git a/platypush/plugins/video/omxplayer.py b/platypush/plugins/video/omxplayer.py new file mode 100644 index 000000000..6812da57f --- /dev/null +++ b/platypush/plugins/video/omxplayer.py @@ -0,0 +1,79 @@ +import json +import re +import subprocess + +from omxplayer import OMXPlayer + +from platypush.message.response import Response + +from .. import Plugin + +class VideoOmxplayerPlugin(Plugin): + def __init__(self, args=[], *argv, **kwargs): + self.args = args + self.player = None + + def play(self, resource): + if resource.startswith('youtube:') \ + or resource.startswith('https://www.youtube.com/watch?v='): + resource = self._get_youtube_content(resource) + + self.player = OMXPlayer(resource, args=self.args) + return self.status() + + def pause(self): + if self.player: self.player.play_pause() + + def stop(self): + if self.player: + self.player.stop() + self.player.quit() + self.player = None + return self.status() + + def voldown(self): + if self.player: + self.player.set_volume(max(-6000, player.set_volume()-1000)) + return self.status() + + def volup(self): + if self.player: + player.set_volume(min(0, player.volume()+1000)) + return self.status() + + def back(self): + if self.player: + self.player.seek(-30) + return self.status() + + def forward(self): + if self.player: + self.player.seek(+30) + return self.status() + + def status(self): + if self.player: + return Response(output=json.dumps({ + 'source': self.player.get_source(), + 'status': self.player.playback_status(), + 'volume': self.player.volume(), + 'elapsed': self.player.position(), + })) + else: + return Response(output=json.dumps({ + 'status': 'Not initialized' + })) + + @classmethod + def _get_youtube_content(cls, url): + m = re.match('youtube:video:(.*)', url) + if m: url = 'https://www.youtube.com/watch?v={}'.format(m.group(1)) + + proc = subprocess.Popen(['youtube-dl','-f','best', '-g', url], + stdout=subprocess.PIPE) + + return proc.stdout.read().decode("utf-8", "strict")[:-1] + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/plugins/video/torrentcast.py b/platypush/plugins/video/torrentcast.py new file mode 100644 index 000000000..40c3a37f1 --- /dev/null +++ b/platypush/plugins/video/torrentcast.py @@ -0,0 +1,44 @@ +import urllib3 +import urllib.request + +from platypush.message.response import Response + +from .. import Plugin + +class VideoTorrentcastPlugin(Plugin): + def __init__(self, server='localhost', port=9090, *args, **kwargs): + self.server = server + self.port = port + + def play(self, url): + request = urllib.request.urlopen( + 'http://{}:{}/play/'.format(self.server, self.port), + data=urllib.parse.urlencode({ + 'url': resource + }) + ) + + return Response(output=request.read()) + + def pause(self): + http = urllib3.PoolManager() + request = http.request('POST', + 'http://{}:{}/pause/'.format(self.server, self.port)) + + return Response(output=request.read()) + + def stop(self): + http = urllib3.PoolManager() + request = http.request('POST', + 'http://{}:{}/stop/'.format(self.server, self.port)) + + return Response(output=request.read()) + + def voldown(self): return Response(output='Unsupported method') + def volup(self): return Response(output='Unsupported method') + def back(self): return Response(output='Unsupported method') + def forward(self): return Response(output='Unsupported method') + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/utils/__init__.py b/platypush/utils/__init__.py index 5ec584983..61178fd4f 100644 --- a/platypush/utils/__init__.py +++ b/platypush/utils/__init__.py @@ -1,45 +1,10 @@ -import functools import importlib import logging import signal -import time from platypush.config import Config from platypush.message import Message -modules = {} - -def get_or_load_plugin(plugin_name, reload=False): - global modules - - if plugin_name in modules and not reload: - return modules[plugin_name] - - try: - module = importlib.import_module('platypush.plugins.' + plugin_name) - except ModuleNotFoundError as e: - logging.warning('No such plugin: {}'.format(plugin_name)) - raise RuntimeError(e) - - # e.g. plugins.music.mpd main class: MusicMpdPlugin - cls_name = functools.reduce( - lambda a,b: a.title() + b.title(), - (plugin_name.title().split('.')) - ) + 'Plugin' - - plugin_conf = Config.get_plugins()[plugin_name] \ - if plugin_name in Config.get_plugins() else {} - - try: - plugin_class = getattr(module, cls_name) - plugin = plugin_class(**plugin_conf) - modules[plugin_name] = plugin - except AttributeError as e: - logging.warning('No such class in {}: {}'.format(plugin_name, cls_name)) - raise RuntimeError(e) - - return plugin - def get_module_and_method_from_action(action): """ Input : action=music.mpd.play diff --git a/setup.py b/setup.py index 2e86b8ad4..044513016 100755 --- a/setup.py +++ b/setup.py @@ -59,7 +59,6 @@ setup( install_requires = [ 'pyyaml', 'requires', - 'websocket-client', ], extras_require = { 'Support for Apache Kafka backend': ['kafka-python'], @@ -68,6 +67,8 @@ setup( 'Support for MPD/Mopidy music server plugin': ['python-mpd2'], 'Support for Belkin WeMo Switch plugin': ['ouimeaux'], 'Support for text2speech plugin': ['mplayer'], + 'Support for OMXPlayer plugin': ['omxplayer'], + 'Support for YouTube in the OMXPlayer plugin': ['youtube-dl'], }, )