Added more general media control plugin, #10
This commit is contained in:
parent
f59a69d86e
commit
92b691041e
8 changed files with 273 additions and 42 deletions
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
94
platypush/plugins/media/ctrl.py
Normal file
94
platypush/plugins/media/ctrl.py
Normal 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:
|
||||
|
|
@ -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())
|
||||
|
||||
|
|
79
platypush/plugins/video/omxplayer.py
Normal file
79
platypush/plugins/video/omxplayer.py
Normal 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:
|
||||
|
44
platypush/plugins/video/torrentcast.py
Normal file
44
platypush/plugins/video/torrentcast.py
Normal 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:
|
||||
|
|
@ -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
|
||||
|
|
3
setup.py
3
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'],
|
||||
},
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue