From 1920bd80a3deeb23716550ad990b0a0507cb1964 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:34:11 +0100 Subject: [PATCH 01/12] First search among the configured media plugins in media.ctrl plugin --- platypush/plugins/media/ctrl.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index cb94e2b8c..8f5adbfbe 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -2,7 +2,7 @@ import re import subprocess from platypush.context import get_plugin -from platypush.plugins.media import PlayerState +from platypush.plugins.media import PlayerState, MediaPlugin from platypush.plugins import Plugin, action @@ -18,10 +18,6 @@ class MediaCtrlPlugin(Plugin): """ - _supported_plugins = { - 'music.mpd', 'media', 'video.torrentcast' - } - def __init__(self, torrentcast_port=9090, *args, **kwargs): super().__init__() self.torrentcast_port = torrentcast_port @@ -62,7 +58,18 @@ class MediaCtrlPlugin(Plugin): if status['state'] == PlayerState.PLAY.value or state['state'] == PlayerState.PAUSE.value: return self.plugin - for plugin in self._supported_plugins: + configured_media_plugins = [ + Config.get(plugin) for plugin in self._supported_media_plugins + if Config.get(plugin) + ] + + if configured_media_plugins: + plugin = get_plugin(plugin) + status = plugin.status().output + if status.get('state') == PlayerState.PLAY.value or status.get('state') == PlayerState.PAUSE.value: + return plugin + + for plugin in MediaPlugin._supported_media_plugins: try: player = get_plugin(plugin) except: From 2bb07ae1919b79ddfd220fefd0251ee41c79d289 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:39:16 +0100 Subject: [PATCH 02/12] Fixed base class name --- platypush/plugins/media/ctrl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index 8f5adbfbe..b280b6145 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -59,7 +59,7 @@ class MediaCtrlPlugin(Plugin): return self.plugin configured_media_plugins = [ - Config.get(plugin) for plugin in self._supported_media_plugins + Config.get(plugin) for plugin in MediaPlugin._supported_media_plugins if Config.get(plugin) ] From a1d6c4fbe46f90f26c9a063ddf4204f9edfb4ae4 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:39:55 +0100 Subject: [PATCH 03/12] Added missing Config import --- platypush/plugins/media/ctrl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index b280b6145..8b396f517 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -1,6 +1,7 @@ import re import subprocess +from platypush.config import Config from platypush.context import get_plugin from platypush.plugins.media import PlayerState, MediaPlugin From 118540db8c0c87d84f774b392c7b992acb4a6019 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:41:06 +0100 Subject: [PATCH 04/12] Fixed unreferenced var --- platypush/plugins/media/ctrl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index 8b396f517..97e65991b 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -65,7 +65,7 @@ class MediaCtrlPlugin(Plugin): ] if configured_media_plugins: - plugin = get_plugin(plugin) + plugin = get_plugin(configured_media_plugins[0]) status = plugin.status().output if status.get('state') == PlayerState.PLAY.value or status.get('state') == PlayerState.PAUSE.value: return plugin From 86ebc4fae922e29174b3e33ab77dc2e089f67396 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:42:20 +0100 Subject: [PATCH 05/12] Get the plugin name, not the object --- platypush/plugins/media/ctrl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index 97e65991b..d24f3892c 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -60,7 +60,7 @@ class MediaCtrlPlugin(Plugin): return self.plugin configured_media_plugins = [ - Config.get(plugin) for plugin in MediaPlugin._supported_media_plugins + plugin for plugin in MediaPlugin._supported_media_plugins if Config.get(plugin) ] From 673351db51674cf8e1b598c6ef1b1b832f53c79a Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:43:26 +0100 Subject: [PATCH 06/12] Don't fail if a media plugin can't be imported --- platypush/plugins/media/ctrl.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index d24f3892c..117b0691a 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -65,7 +65,11 @@ class MediaCtrlPlugin(Plugin): ] if configured_media_plugins: - plugin = get_plugin(configured_media_plugins[0]) + try: + plugin = get_plugin(configured_media_plugins[0]) + except: + pass + status = plugin.status().output if status.get('state') == PlayerState.PLAY.value or status.get('state') == PlayerState.PAUSE.value: return plugin From 346bd9602da14b50867ffd4b2a84a0051e6445e1 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:44:40 +0100 Subject: [PATCH 07/12] The whole get_plugin/.status() block should be under try/except --- platypush/plugins/media/ctrl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index 117b0691a..3a3eb4a05 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -67,10 +67,10 @@ class MediaCtrlPlugin(Plugin): if configured_media_plugins: try: plugin = get_plugin(configured_media_plugins[0]) + status = plugin.status().output except: - pass + continue - status = plugin.status().output if status.get('state') == PlayerState.PLAY.value or status.get('state') == PlayerState.PAUSE.value: return plugin From 31a7ecee03f15537cd70b6c426c92dfeb8aa5eda Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:46:05 +0100 Subject: [PATCH 08/12] Quick fix --- platypush/plugins/media/ctrl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index 3a3eb4a05..fcdc4b048 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -68,11 +68,11 @@ class MediaCtrlPlugin(Plugin): try: plugin = get_plugin(configured_media_plugins[0]) status = plugin.status().output - except: - continue + if status.get('state') == PlayerState.PLAY.value or status.get('state') == PlayerState.PAUSE.value: + return plugin - if status.get('state') == PlayerState.PLAY.value or status.get('state') == PlayerState.PAUSE.value: - return plugin + except: + pass for plugin in MediaPlugin._supported_media_plugins: try: From 7e1d2329424c6d85126b9c9616901452acc6ebe6 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 17:47:33 +0100 Subject: [PATCH 09/12] Another fix bites the dust --- platypush/plugins/media/ctrl.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py index fcdc4b048..e82702d36 100644 --- a/platypush/plugins/media/ctrl.py +++ b/platypush/plugins/media/ctrl.py @@ -80,13 +80,12 @@ class MediaCtrlPlugin(Plugin): except: try: player = get_plugin(plugin, reload=True) + status = player.status().output + if status['state'] == PlayerState.PLAY.value or status['state'] == PlayerState.PAUSE.value: + return player except: continue - status = player.status().output - if status['state'] == PlayerState.PLAY.value or status['state'] == PlayerState.PAUSE.value: - return player - return None From 7f24b82281e85563e747c0962d6c8a94637a3b29 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 3 Feb 2021 22:13:10 +0100 Subject: [PATCH 10/12] Removed deprecated media.ctrl plugin --- platypush/plugins/media/ctrl.py | 173 -------------------------------- 1 file changed, 173 deletions(-) delete mode 100644 platypush/plugins/media/ctrl.py diff --git a/platypush/plugins/media/ctrl.py b/platypush/plugins/media/ctrl.py deleted file mode 100644 index e82702d36..000000000 --- a/platypush/plugins/media/ctrl.py +++ /dev/null @@ -1,173 +0,0 @@ -import re -import subprocess - -from platypush.config import Config -from platypush.context import get_plugin -from platypush.plugins.media import PlayerState, MediaPlugin - -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] - - """ - - 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 - - configured_media_plugins = [ - plugin for plugin in MediaPlugin._supported_media_plugins - if Config.get(plugin) - ] - - if configured_media_plugins: - try: - plugin = get_plugin(configured_media_plugins[0]) - status = plugin.status().output - if status.get('state') == PlayerState.PLAY.value or status.get('state') == PlayerState.PAUSE.value: - return plugin - - except: - pass - - for plugin in MediaPlugin._supported_media_plugins: - try: - player = get_plugin(plugin) - except: - try: - player = get_plugin(plugin, reload=True) - status = player.status().output - if status['state'] == PlayerState.PLAY.value or status['state'] == PlayerState.PAUSE.value: - return player - except: - continue - - 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: - From 165f85f8dc1556281b86ebde53e680c3f8e1d7b7 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 4 Feb 2021 01:43:51 +0100 Subject: [PATCH 11/12] Added support for background shell commands --- platypush/plugins/shell/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platypush/plugins/shell/__init__.py b/platypush/plugins/shell/__init__.py index b6f7b8c98..dfb100186 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') From 2834ed2a7c609047f459f787d45703009a9ab3b7 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 4 Feb 2021 01:44:21 +0100 Subject: [PATCH 12/12] Refactored and fixed many bugs on the media.omxplayer plugin --- platypush/plugins/media/__init__.py | 5 +- platypush/plugins/media/omxplayer.py | 96 ++++++++++++++++++---------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py index bc32cd7a5..7488787e6 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): """ @@ -477,7 +478,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/omxplayer.py b/platypush/plugins/media/omxplayer.py index ed62e53a5..299633ae0 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: