Merge branch 'master' into vuejs
This commit is contained in:
commit
2abfb2964c
4 changed files with 69 additions and 200 deletions
|
@ -162,7 +162,8 @@ class MediaPlugin(Plugin):
|
||||||
def _is_youtube_resource(resource):
|
def _is_youtube_resource(resource):
|
||||||
return resource.startswith('youtube:') \
|
return resource.startswith('youtube:') \
|
||||||
or resource.startswith('https://youtu.be/') \
|
or resource.startswith('https://youtu.be/') \
|
||||||
or resource.startswith('https://www.youtube.com/watch?v=')
|
or resource.startswith('https://www.youtube.com/watch?v=') \
|
||||||
|
or resource.startswith('https://youtube.com/watch?v=')
|
||||||
|
|
||||||
def _get_resource(self, resource):
|
def _get_resource(self, resource):
|
||||||
"""
|
"""
|
||||||
|
@ -482,7 +483,7 @@ class MediaPlugin(Plugin):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_youtube_video_url(url):
|
def get_youtube_video_url(url):
|
||||||
youtube_dl = subprocess.Popen(['youtube-dl', '-f', 'best', '-g', url], stdout=subprocess.PIPE)
|
youtube_dl = subprocess.Popen(['youtube-dl', '-f', 'best', '-g', url], stdout=subprocess.PIPE)
|
||||||
url = youtube_dl.communicate()[0].decode()
|
url = youtube_dl.communicate()[0].decode().strip()
|
||||||
youtube_dl.wait()
|
youtube_dl.wait()
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
|
@ -1,162 +0,0 @@
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from platypush.context import get_plugin
|
|
||||||
from platypush.plugins.media import PlayerState
|
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
|
||||||
|
|
||||||
class MediaCtrlPlugin(Plugin):
|
|
||||||
"""
|
|
||||||
Wrapper plugin to control audio and video media.
|
|
||||||
Examples of supported URL types:
|
|
||||||
|
|
||||||
- file:///media/movies/Movie.mp4 [requires media plugin enabled]
|
|
||||||
- youtube:video:poAk9XgK7Cs [requires media plugin+youtube-dl]
|
|
||||||
- magnet:?torrent_magnet [requires torrentcast]
|
|
||||||
- spotify:track:track_id [leverages plugins.music.mpd]
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
_supported_plugins = {
|
|
||||||
'music.mpd', 'media', 'video.torrentcast'
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, torrentcast_port=9090, *args, **kwargs):
|
|
||||||
super().__init__()
|
|
||||||
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.search('^https://open.spotify.com/([^?]+)', url)
|
|
||||||
if m: url = 'spotify:{}'.format(m.group(1).replace('/', ':'))
|
|
||||||
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 _get_playing_plugin(self):
|
|
||||||
if self.plugin:
|
|
||||||
status = self.plugin.status()
|
|
||||||
if status['state'] == PlayerState.PLAY.value or state['state'] == PlayerState.PAUSE.value:
|
|
||||||
return self.plugin
|
|
||||||
|
|
||||||
for plugin in self._supported_plugins:
|
|
||||||
try:
|
|
||||||
player = get_plugin(plugin)
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
player = get_plugin(plugin, reload=True)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
status = player.status().output
|
|
||||||
if status['state'] == PlayerState.PLAY.value or status['state'] == PlayerState.PAUSE.value:
|
|
||||||
return player
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def play(self, url):
|
|
||||||
(type, resource) = self._get_type_and_resource_by_url(url)
|
|
||||||
plugin_name = None
|
|
||||||
|
|
||||||
if type == 'mpd':
|
|
||||||
plugin_name = 'music.mpd'
|
|
||||||
elif type == 'youtube:video' or type == 'file':
|
|
||||||
plugin_name = 'media'
|
|
||||||
elif type == 'torrent':
|
|
||||||
plugin_name = 'video.torrentcast'
|
|
||||||
|
|
||||||
if not plugin_name:
|
|
||||||
raise RuntimeError("Unsupported type '{}'".format(type))
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.plugin = get_plugin(plugin_name)
|
|
||||||
except:
|
|
||||||
self.plugin = get_plugin(plugin_name, reload=True)
|
|
||||||
|
|
||||||
self.url = resource
|
|
||||||
return self.plugin.play(resource)
|
|
||||||
|
|
||||||
@action
|
|
||||||
def pause(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin: return plugin.pause()
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def stop(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin:
|
|
||||||
ret = plugin.stop()
|
|
||||||
self.plugin = None
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def voldown(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin: return plugin.voldown()
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def volup(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin: return plugin.volup()
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def back(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin: return plugin.back()
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def forward(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin: return plugin.forward()
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def next(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin: return plugin.next()
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def previous(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin: return plugin.previous()
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
|
||||||
def status(self):
|
|
||||||
plugin = self._get_playing_plugin()
|
|
||||||
if plugin: return plugin.status()
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import enum
|
import enum
|
||||||
|
import math
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from platypush.context import get_bus
|
from platypush.context import get_bus
|
||||||
from platypush.plugins.media import MediaPlugin, PlayerState
|
from platypush.plugins.media import MediaPlugin, PlayerState
|
||||||
from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, MediaStopEvent, MediaSeekEvent
|
from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, MediaStopEvent, MediaSeekEvent, MediaPlayRequestEvent
|
||||||
|
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
|
|
||||||
|
@ -43,9 +44,9 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
self._handlers = {e.value: [] for e in PlayerEvent}
|
self._handlers = {e.value: [] for e in PlayerEvent}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource, subtitles=None, *args, **kwargs):
|
def play(self, resource=None, subtitles=None, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Play a resource.
|
Play or resume playing a resource.
|
||||||
|
|
||||||
:param resource: Resource to play. Supported types:
|
:param resource: Resource to play. Supported types:
|
||||||
|
|
||||||
|
@ -56,6 +57,15 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
|
|
||||||
:param subtitles: Subtitles file
|
:param subtitles: Subtitles file
|
||||||
"""
|
"""
|
||||||
|
if not resource:
|
||||||
|
if not self._player:
|
||||||
|
self.logger.warning('No OMXPlayer instances running')
|
||||||
|
else:
|
||||||
|
self._player.play()
|
||||||
|
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
self._post_event(MediaPlayRequestEvent, resource=resource)
|
||||||
|
|
||||||
if subtitles:
|
if subtitles:
|
||||||
args += ('--subtitles', subtitles)
|
args += ('--subtitles', subtitles)
|
||||||
|
@ -75,17 +85,17 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
try:
|
try:
|
||||||
from omxplayer import OMXPlayer
|
from omxplayer import OMXPlayer
|
||||||
self._player = OMXPlayer(resource, args=self.args)
|
self._player = OMXPlayer(resource, args=self.args)
|
||||||
self._init_player_handlers()
|
|
||||||
except DBusException as e:
|
except DBusException as e:
|
||||||
self.logger.warning('DBus connection failed: you will probably not ' +
|
self.logger.warning('DBus connection failed: you will probably not ' +
|
||||||
'be able to control the media')
|
'be able to control the media')
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
|
|
||||||
self._player.pause()
|
|
||||||
if self.volume:
|
if self.volume:
|
||||||
self.set_volume(volume=self.volume)
|
self.set_volume(self.volume)
|
||||||
|
|
||||||
|
self._post_event(MediaPlayEvent, resource=resource)
|
||||||
|
self._init_player_handlers()
|
||||||
|
|
||||||
self._player.play()
|
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -111,35 +121,52 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
self._player.quit()
|
self._player.quit()
|
||||||
except OMXPlayerDeadError:
|
except OMXPlayerDeadError:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
self._player = None
|
self._player = None
|
||||||
|
|
||||||
return {'status': 'stop'}
|
return {'status': 'stop'}
|
||||||
|
|
||||||
@action
|
def get_volume(self) -> float:
|
||||||
def voldown(self):
|
"""
|
||||||
""" Volume down by 10% """
|
:return: The player volume in percentage [0, 100].
|
||||||
|
"""
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.set_volume(max(0, self._player.volume()-0.1))
|
return self._player.volume()*100
|
||||||
|
|
||||||
|
@action
|
||||||
|
def voldown(self, step=10.0):
|
||||||
|
"""
|
||||||
|
Decrease the volume.
|
||||||
|
|
||||||
|
:param step: Volume decrease step between 0 and 100 (default: 10%).
|
||||||
|
:type step: float
|
||||||
|
"""
|
||||||
|
if self._player:
|
||||||
|
self.set_volume(max(0, self.get_volume()-step))
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def volup(self):
|
def volup(self, step=10.0):
|
||||||
""" Volume up by 10% """
|
"""
|
||||||
|
Increase the volume.
|
||||||
|
|
||||||
|
:param step: Volume increase step between 0 and 100 (default: 10%).
|
||||||
|
:type step: float
|
||||||
|
"""
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.set_volume(min(1, self._player.volume()+0.1))
|
self.set_volume(min(100, self.get_volume()+step))
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def back(self, offset=60):
|
def back(self, offset=30):
|
||||||
""" Back by (default: 60) seconds """
|
""" Back by (default: 30) seconds """
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.seek(-offset)
|
self._player.seek(-offset)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def forward(self, offset=60):
|
def forward(self, offset=30):
|
||||||
""" Forward by (default: 60) seconds """
|
""" Forward by (default: 30) seconds """
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.seek(offset)
|
self._player.seek(offset)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
@ -216,30 +243,27 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seek(self, relative_position):
|
def seek(self, position):
|
||||||
"""
|
"""
|
||||||
Seek backward/forward by the specified number of seconds
|
Seek to the specified number of seconds from the start.
|
||||||
|
|
||||||
:param relative_position: Number of seconds relative to the current cursor
|
:param position: Number of seconds from the start
|
||||||
:type relative_position: int
|
:type position: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._player:
|
if self._player:
|
||||||
self._player.seek(relative_position)
|
self._player.set_position(position)
|
||||||
return self.status()
|
return self.status()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_position(self, position):
|
def set_position(self, position):
|
||||||
"""
|
"""
|
||||||
Seek backward/forward to the specified absolute position
|
Seek to the specified number of seconds from the start (same as :meth:`.seek`).
|
||||||
|
|
||||||
:param position: Number of seconds from the start
|
:param position: Number of seconds from the start
|
||||||
:type position: int
|
:type position: float
|
||||||
"""
|
"""
|
||||||
|
return self.seek(position)
|
||||||
|
|
||||||
if self._player:
|
|
||||||
self._player.seek(position)
|
|
||||||
return self.status()
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_volume(self, volume):
|
def set_volume(self, volume):
|
||||||
|
@ -247,7 +271,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
Set the volume
|
Set the volume
|
||||||
|
|
||||||
:param volume: Volume value between 0 and 100
|
:param volume: Volume value between 0 and 100
|
||||||
:type volume: int
|
:type volume: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._player:
|
if self._player:
|
||||||
|
@ -314,7 +338,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
'state': state,
|
'state': state,
|
||||||
'title': urllib.parse.unquote(self._player.get_source()).split('/')[-1] if self._player.get_source().startswith('file://') else None,
|
'title': urllib.parse.unquote(self._player.get_source()).split('/')[-1] if self._player.get_source().startswith('file://') else None,
|
||||||
'url': self._player.get_source(),
|
'url': self._player.get_source(),
|
||||||
'volume': self._player.volume() * 100,
|
'volume': self.get_volume(),
|
||||||
'volume_max': 100,
|
'volume_max': 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,14 +373,14 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
return _f
|
return _f
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
def _f(player):
|
def _f(player, *_, **__):
|
||||||
self._post_event(MediaStopEvent)
|
self._post_event(MediaStopEvent)
|
||||||
for callback in self._handlers[PlayerEvent.STOP.value]:
|
for callback in self._handlers[PlayerEvent.STOP.value]:
|
||||||
callback()
|
callback()
|
||||||
return _f
|
return _f
|
||||||
|
|
||||||
def on_seek(self):
|
def on_seek(self):
|
||||||
def _f(player):
|
def _f(player, *_, **__):
|
||||||
self._post_event(MediaSeekEvent, position=player.position())
|
self._post_event(MediaSeekEvent, position=player.position())
|
||||||
return _f
|
return _f
|
||||||
|
|
||||||
|
@ -367,7 +391,9 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
self._player.playEvent += self.on_play()
|
self._player.playEvent += self.on_play()
|
||||||
self._player.pauseEvent += self.on_pause()
|
self._player.pauseEvent += self.on_pause()
|
||||||
self._player.stopEvent += self.on_stop()
|
self._player.stopEvent += self.on_stop()
|
||||||
|
self._player.exitEvent += self.on_stop()
|
||||||
self._player.positionEvent += self.on_seek()
|
self._player.positionEvent += self.on_seek()
|
||||||
|
self._player.seekEvent += self.on_seek()
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -9,17 +9,21 @@ class ShellPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def exec(self, cmd, ignore_errors=False):
|
def exec(self, cmd, background=False, ignore_errors=False):
|
||||||
"""
|
"""
|
||||||
Execute a command.
|
Execute a command.
|
||||||
|
|
||||||
:param cmd: Command to execute
|
:param cmd: Command to execute
|
||||||
:type cmd: str
|
:type cmd: str
|
||||||
|
|
||||||
|
:param background: If set to True, execute the process in the background, otherwise wait for the process termination and return its output (deafult: False).
|
||||||
:param ignore_errors: If set, then any errors in the command execution will be ignored. Otherwise a RuntimeError will be thrown (default value: False)
|
:param ignore_errors: If set, then any errors in the command execution will be ignored. Otherwise a RuntimeError will be thrown (default value: False)
|
||||||
:returns: A response object where the ``output`` field will contain the command output as a string, and the ``errors`` field will contain whatever was sent to stderr.
|
:returns: A response object where the ``output`` field will contain the command output as a string, and the ``errors`` field will contain whatever was sent to stderr.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if background:
|
||||||
|
subprocess.Popen(cmd, shell=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return subprocess.check_output(
|
return subprocess.check_output(
|
||||||
cmd, stderr=subprocess.STDOUT, shell=True).decode('utf-8')
|
cmd, stderr=subprocess.STDOUT, shell=True).decode('utf-8')
|
||||||
|
|
Loading…
Reference in a new issue