Added more general media control plugin, #10

This commit is contained in:
Fabio Manganiello 2017-12-27 10:18:51 +01:00
parent f59a69d86e
commit 92b691041e
8 changed files with 273 additions and 42 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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())

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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'],
},
)