diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py index 7fae363e..cc54ddcc 100644 --- a/platypush/plugins/media/__init__.py +++ b/platypush/plugins/media/__init__.py @@ -162,7 +162,8 @@ class MediaPlugin(Plugin): def _is_youtube_resource(resource): return resource.startswith('youtube:') \ 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): """ @@ -482,7 +483,7 @@ class MediaPlugin(Plugin): @staticmethod def get_youtube_video_url(url): 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() return url diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py deleted file mode 100644 index cb94e2b8..00000000 --- a/platypush/plugins/media/ctrl.py +++ /dev/null @@ -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: - diff --git a/platypush/plugins/media/omxplayer.py b/platypush/plugins/media/omxplayer.py index ed62e53a..299633ae 100644 --- a/platypush/plugins/media/omxplayer.py +++ b/platypush/plugins/media/omxplayer.py @@ -1,9 +1,10 @@ import enum +import math import urllib.parse from platypush.context import get_bus 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 @@ -43,9 +44,9 @@ class MediaOmxplayerPlugin(MediaPlugin): self._handlers = {e.value: [] for e in PlayerEvent} @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: @@ -56,6 +57,15 @@ class MediaOmxplayerPlugin(MediaPlugin): :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: args += ('--subtitles', subtitles) @@ -75,17 +85,17 @@ class MediaOmxplayerPlugin(MediaPlugin): try: from omxplayer import OMXPlayer self._player = OMXPlayer(resource, args=self.args) - self._init_player_handlers() except DBusException as e: self.logger.warning('DBus connection failed: you will probably not ' + 'be able to control the media') self.logger.exception(e) - self._player.pause() 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() @action @@ -111,35 +121,52 @@ class MediaOmxplayerPlugin(MediaPlugin): self._player.quit() except OMXPlayerDeadError: pass - - self._player = None + finally: + self._player = None return {'status': 'stop'} - @action - def voldown(self): - """ Volume down by 10% """ + def get_volume(self) -> float: + """ + :return: The player volume in percentage [0, 100]. + """ 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() @action - def volup(self): - """ Volume up by 10% """ + def volup(self, step=10.0): + """ + Increase the volume. + + :param step: Volume increase step between 0 and 100 (default: 10%). + :type step: float + """ 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() @action - def back(self, offset=60): - """ Back by (default: 60) seconds """ + def back(self, offset=30): + """ Back by (default: 30) seconds """ if self._player: self._player.seek(-offset) return self.status() @action - def forward(self, offset=60): - """ Forward by (default: 60) seconds """ + def forward(self, offset=30): + """ Forward by (default: 30) seconds """ if self._player: self._player.seek(offset) return self.status() @@ -216,30 +243,27 @@ class MediaOmxplayerPlugin(MediaPlugin): return self.status() @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 - :type relative_position: int + :param position: Number of seconds from the start + :type position: float """ - if self._player: - self._player.seek(relative_position) + self._player.set_position(position) return self.status() @action 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 - :type position: int + :type position: float """ + return self.seek(position) - if self._player: - self._player.seek(position) - return self.status() @action def set_volume(self, volume): @@ -247,7 +271,7 @@ class MediaOmxplayerPlugin(MediaPlugin): Set the volume :param volume: Volume value between 0 and 100 - :type volume: int + :type volume: float """ if self._player: @@ -314,7 +338,7 @@ class MediaOmxplayerPlugin(MediaPlugin): 'state': state, 'title': urllib.parse.unquote(self._player.get_source()).split('/')[-1] if self._player.get_source().startswith('file://') else None, 'url': self._player.get_source(), - 'volume': self._player.volume() * 100, + 'volume': self.get_volume(), 'volume_max': 100, } @@ -349,14 +373,14 @@ class MediaOmxplayerPlugin(MediaPlugin): return _f def on_stop(self): - def _f(player): + def _f(player, *_, **__): self._post_event(MediaStopEvent) for callback in self._handlers[PlayerEvent.STOP.value]: callback() return _f def on_seek(self): - def _f(player): + def _f(player, *_, **__): self._post_event(MediaSeekEvent, position=player.position()) return _f @@ -367,7 +391,9 @@ class MediaOmxplayerPlugin(MediaPlugin): self._player.playEvent += self.on_play() self._player.pauseEvent += self.on_pause() self._player.stopEvent += self.on_stop() + self._player.exitEvent += self.on_stop() self._player.positionEvent += self.on_seek() + self._player.seekEvent += self.on_seek() # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/shell/__init__.py b/platypush/plugins/shell/__init__.py index b6f7b8c9..dfb10018 100644 --- a/platypush/plugins/shell/__init__.py +++ b/platypush/plugins/shell/__init__.py @@ -9,17 +9,21 @@ class ShellPlugin(Plugin): """ @action - def exec(self, cmd, ignore_errors=False): + def exec(self, cmd, background=False, ignore_errors=False): """ Execute a command. :param cmd: Command to execute :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) :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: return subprocess.check_output( cmd, stderr=subprocess.STDOUT, shell=True).decode('utf-8')