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]
|
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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
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)
|
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())
|
||||||
|
|
||||||
|
|
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 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
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -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'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue