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] 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: # vim:sw=4:ts=4:et:

View file

@ -5,9 +5,10 @@ import traceback
from threading import Thread from threading import Thread
from platypush.context import get_plugin
from platypush.message import Message from platypush.message import Message
from platypush.message.response import Response 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): class Request(Message):
""" Request message class """ """ Request message class """
@ -59,7 +60,7 @@ class Request(Message):
def _thread_func(n_tries): def _thread_func(n_tries):
(module_name, method_name) = get_module_and_method_from_action(self.action) (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: try:
# Run the action # Run the action
@ -75,7 +76,7 @@ class Request(Message):
logging.exception(e) logging.exception(e)
if n_tries: if n_tries:
logging.info('Reloading plugin {} and retrying'.format(module_name)) 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) _thread_func(n_tries-1)
return return
finally: 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) getattr(self.client, method)(*args, **kwargs)
return self.status() return self.status()
def play(self): def play(self, resource=None):
if resource:
self.clear()
self.add(resource)
return self._exec('play') return self._exec('play')
def pause(self): def pause(self):
@ -56,8 +59,8 @@ class MusicMpdPlugin(MusicPlugin):
self.setvol(str(new_volume)) self.setvol(str(new_volume))
return self.status() return self.status()
def add(self, content): def add(self, resource):
return self._exec('add', content) return self._exec('add', resource)
def playlistadd(self, playlist): def playlistadd(self, playlist):
return self._exec('playlistadd', playlist) return self._exec('playlistadd', playlist)
@ -65,6 +68,12 @@ class MusicMpdPlugin(MusicPlugin):
def clear(self): def clear(self):
return self._exec('clear') return self._exec('clear')
def forward(self):
return self._exec('seekcur', '+15')
def back(self):
return self._exec('seekcur', '-15')
def status(self): def status(self):
return Response(output=self.client.status()) 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 importlib
import logging import logging
import signal import signal
import time
from platypush.config import Config from platypush.config import Config
from platypush.message import Message 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): def get_module_and_method_from_action(action):
""" Input : action=music.mpd.play """ Input : action=music.mpd.play

View file

@ -59,7 +59,6 @@ setup(
install_requires = [ install_requires = [
'pyyaml', 'pyyaml',
'requires', 'requires',
'websocket-client',
], ],
extras_require = { extras_require = {
'Support for Apache Kafka backend': ['kafka-python'], 'Support for Apache Kafka backend': ['kafka-python'],
@ -68,6 +67,8 @@ setup(
'Support for MPD/Mopidy music server plugin': ['python-mpd2'], 'Support for MPD/Mopidy music server plugin': ['python-mpd2'],
'Support for Belkin WeMo Switch plugin': ['ouimeaux'], 'Support for Belkin WeMo Switch plugin': ['ouimeaux'],
'Support for text2speech plugin': ['mplayer'], 'Support for text2speech plugin': ['mplayer'],
'Support for OMXPlayer plugin': ['omxplayer'],
'Support for YouTube in the OMXPlayer plugin': ['youtube-dl'],
}, },
) )