From dc2a686d238efd0da2a0a93501a19b22999ae673 Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <blacklight86@gmail.com>
Date: Tue, 5 Feb 2019 02:30:20 +0100
Subject: [PATCH] Support for casting torrents to Chromecast

---
 platypush/plugins/media/__init__.py   |  24 +++--
 platypush/plugins/media/chromecast.py | 127 ++++++++++++++++----------
 platypush/plugins/media/mplayer.py    |   4 +-
 platypush/plugins/media/webtorrent.py |  44 ++++++---
 platypush/utils/__init__.py           |   6 ++
 5 files changed, 134 insertions(+), 71 deletions(-)

diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py
index 6d8ec6b5..77ce7fed 100644
--- a/platypush/plugins/media/__init__.py
+++ b/platypush/plugins/media/__init__.py
@@ -22,7 +22,7 @@ class MediaPlugin(Plugin):
 
     Requires:
 
-        * A media player installed (supported so far: mplayer, omxplayer)
+        * A media player installed (supported so far: mplayer, omxplayer, chromecast)
         * **python-libtorrent** (``pip install python-libtorrent``), optional for Torrent support
         * **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support
     """
@@ -53,7 +53,8 @@ class MediaPlugin(Plugin):
         'f4b',
     }
 
-    _supported_media_plugins = { 'media.mplayer', 'media.omxplayer' }
+    _supported_media_plugins = {'media.mplayer', 'media.omxplayer',
+                                'media.chromecast'}
 
     def __init__(self, media_dirs=[], download_dir=None, env=None,
                  *args, **kwargs):
@@ -75,10 +76,17 @@ class MediaPlugin(Plugin):
 
         player = None
         player_config = {}
-        for plugin in Config.get_plugins().keys():
-            if plugin in self._supported_media_plugins:
-                player = plugin
-                break
+
+        if self.__class__.__name__ == 'MediaPlugin':
+            # Abstract class, initialize with the default configured player
+            for plugin in Config.get_plugins().keys():
+                if plugin in self._supported_media_plugins:
+                    player = plugin
+                    if get_plugin(player).is_local():
+                        # Local players have priority as default if configured
+                        break
+        else:
+            player = self  # Derived concrete class
 
         if not player:
             raise AttributeError('No media plugin configured')
@@ -126,6 +134,10 @@ class MediaPlugin(Plugin):
 
         if resource.startswith('youtube:') \
                 or resource.startswith('https://www.youtube.com/watch?v='):
+            if self.__class.__.__name__ != 'MediaChromecastPlugin':
+                # The Chromecast has already its way to handle YouTube
+                return resource
+
             resource = self._get_youtube_content(resource)
         elif resource.startswith('magnet:?'):
             try:
diff --git a/platypush/plugins/media/chromecast.py b/platypush/plugins/media/chromecast.py
index 04dbe90c..f30be42f 100644
--- a/platypush/plugins/media/chromecast.py
+++ b/platypush/plugins/media/chromecast.py
@@ -3,15 +3,18 @@ import pychromecast
 
 from pychromecast.controllers.youtube import YouTubeController
 
+from platypush.context import get_plugin
 from platypush.plugins import Plugin, action
+from platypush.plugins.media import MediaPlugin
 
 
-class MediaChromecastPlugin(Plugin):
+class MediaChromecastPlugin(MediaPlugin):
     """
     Plugin to interact with Chromecast devices
 
     Supported formats:
 
+        * HTTP media URLs
         * YouTube URLs
         * Plex (through ``media.plex`` plugin, experimental)
 
@@ -32,6 +35,7 @@ class MediaChromecastPlugin(Plugin):
 
         super().__init__(*args, **kwargs)
 
+        self._is_local = False
         self.chromecast = chromecast
         self.chromecasts = {}
 
@@ -67,7 +71,10 @@ class MediaChromecastPlugin(Plugin):
         } for cc in pychromecast.get_chromecasts() ]
 
 
-    def get_chromecast(self, chromecast=None):
+    def get_chromecast(self, chromecast=None, n_tries=3):
+        if isinstance(chromecast, pychromecast.Chromecast):
+            return chromecast
+
         if not chromecast:
             if not self.chromecast:
                 raise RuntimeError('No Chromecast specified nor default Chromecast configured')
@@ -75,10 +82,17 @@ class MediaChromecastPlugin(Plugin):
 
 
         if chromecast not in self.chromecasts:
-            self.chromecasts = {
-                cast.device.friendly_name: cast
-                for cast in pychromecast.get_chromecasts()
-            }
+            casts = {}
+            while n_tries > 0:
+                n_tries -= 1
+                casts.update({
+                    cast.device.friendly_name: cast
+                    for cast in pychromecast.get_chromecasts()
+                })
+
+                if chromecast in casts:
+                    self.chromecasts.update(casts)
+                    break
 
             if chromecast not in self.chromecasts:
                 raise RuntimeError('Device {} not found'.format(chromecast))
@@ -87,7 +101,7 @@ class MediaChromecastPlugin(Plugin):
 
 
     @action
-    def cast_media(self, media, content_type=None, chromecast=None, title=None,
+    def play(self, resource, content_type=None, chromecast=None, title=None,
                    image_url=None, autoplay=True, current_time=0,
                    stream_type=STREAM_TYPE_BUFFERED, subtitles=None,
                    subtitles_lang='en-US', subtitles_mime='text/vtt',
@@ -95,10 +109,10 @@ class MediaChromecastPlugin(Plugin):
         """
         Cast media to a visible Chromecast
 
-        :param media: Media to cast
-        :type media: str
+        :param resource: Media to cast
+        :type resource: str
 
-        :param content_type: Content type
+        :param content_type: Content type as a MIME type string
         :type content_type: str
 
         :param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
@@ -139,7 +153,7 @@ class MediaChromecastPlugin(Plugin):
         cast.wait()
 
         mc = cast.media_controller
-        yt = self._get_youtube_url(media)
+        yt = self._get_youtube_url(resource)
 
         if yt:
             self.logger.info('Playing YouTube video {} on {}'.format(
@@ -150,13 +164,33 @@ class MediaChromecastPlugin(Plugin):
             hndl.update_screen_id()
             return hndl.play_video(yt)
 
+        resource = self._get_resource(resource)
+        if resource.startswith('magnet:?'):
+            player_args = { 'chromecast': cast }
+            return get_plugin('media.webtorrent').play(resource,
+                                                       player='chromecast',
+                                                       **player_args)
+
+        # Best effort from the extension
+        if not content_type:
+            for ext in self.video_extensions:
+                if ('.' + ext).lower() in resource.lower():
+                    content_type = 'video/' + ext
+                    break
+
+        if not content_type:
+            for ext in self.audio_extensions:
+                if ('.' + ext).lower() in resource.lower():
+                    content_type = 'audio/' + ext
+                    break
+
         if not content_type:
             raise RuntimeError('content_type required to process media {}'.
-                               format(media))
+                               format(resource))
 
-        self.logger.info('Playing {} on {}'.format(media, chromecast))
+        self.logger.info('Playing {} on {}'.format(resource, chromecast))
 
-        mc.play_media(media, content_type, title=title, thumb=image_url,
+        mc.play_media(resource, content_type, title=title, thumb=image_url,
                       current_time=current_time, autoplay=autoplay,
                       stream_type=stream_type, subtitles=subtitles,
                       subtitles_lang=subtitles_lang, subtitles_mime=subtitles_mime,
@@ -177,19 +211,12 @@ class MediaChromecastPlugin(Plugin):
 
         return None
 
-
     @action
-    def play(self, chromecast=None):
-        return self.get_chromecast(chromecast or self.chromecast).media_controller.play()
-
+    def load(self, *args, **kwargs):
+        return self.play(*args, **kwargs)
 
     @action
     def pause(self, chromecast=None):
-        return self.get_chromecast(chromecast or self.chromecast).media_controller.pause()
-
-
-    @action
-    def toggle_pause(self, chromecast=None):
         cast = self.get_chromecast(chromecast or self.chromecast)
         if cast.media_controller.is_paused:
             return cast.media_controller.play()
@@ -208,22 +235,25 @@ class MediaChromecastPlugin(Plugin):
 
 
     @action
-    def seek(self, location, chromecast=None):
-        return self.get_chromecast(chromecast or self.chromecast).media_controller.seek(location)
+    def set_position(self, position, chromecast=None):
+        return self.get_chromecast(chromecast or self.chromecast).media_controller.seek(position)
+
+    @action
+    def seek(self, position, chromecast=None):
+        return self.forward(chromecast=chromecast, offset=position)
+
+    @action
+    def back(self, chromecast=None, offset=60):
+        mc = self.get_chromecast(chromecast or self.chromecast).media_controller
+        if mc.status.current_time:
+            return mc.seek(mc.status.current_time-offset)
 
 
     @action
-    def back(self, chromecast=None, delta=30):
+    def forward(self, chromecast=None, offset=60):
         mc = self.get_chromecast(chromecast or self.chromecast).media_controller
         if mc.status.current_time:
-            return mc.seek(mc.status.current_time-delta)
-
-
-    @action
-    def forward(self, chromecast=None, delta=30):
-        mc = self.get_chromecast(chromecast or self.chromecast).media_controller
-        if mc.status.current_time:
-            return mc.seek(mc.status.current_time+delta)
+            return mc.seek(mc.status.current_time+offset)
 
 
     @action
@@ -288,7 +318,7 @@ class MediaChromecastPlugin(Plugin):
         cast.join(timeout=timeout, blocking=blocking)
 
     @action
-    def quit_app(self, chromecast=None):
+    def quit(self, chromecast=None):
         """
         Exits the current app on the Chromecast
 
@@ -327,41 +357,41 @@ class MediaChromecastPlugin(Plugin):
         cast.set_volume(volume/100)
 
     @action
-    def volume_up(self, chromecast=None, delta=10):
+    def volup(self, chromecast=None, step=10):
         """
-        Turn up the Chromecast volume by 10% or delta.
+        Turn up the Chromecast volume by 10% or step.
 
         :param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
         :type chromecast: str
 
-        :param delta: Volume increment between 0 and 100 (default: 100%)
-        :type delta: float
+        :param step: Volume increment between 0 and 100 (default: 100%)
+        :type step: float
         """
 
         cast = self.get_chromecast(chromecast)
-        delta /= 100
-        cast.volume_up(min(delta, 1))
+        step /= 100
+        cast.volume_up(min(step, 1))
 
 
     @action
-    def volume_down(self, chromecast=None, delta=10):
+    def voldown(self, chromecast=None, step=10):
         """
-        Turn down the Chromecast volume by 10% or delta.
+        Turn down the Chromecast volume by 10% or step.
 
         :param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
         :type chromecast: str
 
-        :param delta: Volume decrement between 0 and 100 (default: 100%)
-        :type delta: float
+        :param step: Volume decrement between 0 and 100 (default: 100%)
+        :type step: float
         """
 
         cast = self.get_chromecast(chromecast)
-        delta /= 100
-        cast.volume_down(max(delta, 0))
+        step /= 100
+        cast.volume_down(max(step, 0))
 
 
     @action
-    def toggle_mute(self, chromecast=None):
+    def mute(self, chromecast=None):
         """
         Toggle the mute status on the Chromecast
 
@@ -374,4 +404,3 @@ class MediaChromecastPlugin(Plugin):
 
 
 # vim:sw=4:ts=4:et:
-
diff --git a/platypush/plugins/media/mplayer.py b/platypush/plugins/media/mplayer.py
index 4f480b29..ac81c2c6 100644
--- a/platypush/plugins/media/mplayer.py
+++ b/platypush/plugins/media/mplayer.py
@@ -330,14 +330,14 @@ class MediaMplayerPlugin(MediaPlugin):
         return self._exec('mute')
 
     @action
-    def seek(self, relative_position):
+    def seek(self, position):
         """
         Seek backward/forward by the specified number of seconds
 
         :param relative_position: Number of seconds relative to the current cursor
         :type relative_position: int
         """
-        return self.step_property('time_pos', offset)
+        return self.step_property('time_pos', position)
 
     @action
     def set_position(self, position):
diff --git a/platypush/plugins/media/webtorrent.py b/platypush/plugins/media/webtorrent.py
index 71818b72..ae133596 100644
--- a/platypush/plugins/media/webtorrent.py
+++ b/platypush/plugins/media/webtorrent.py
@@ -17,7 +17,7 @@ from platypush.message.event.torrent import TorrentDownloadStartEvent, \
 
 from platypush.plugins import action
 from platypush.utils import find_bins_in_path, find_files_by_ext, \
-    is_process_alive
+    is_process_alive, get_ip_or_hostname
 
 
 class TorrentState(enum.Enum):
@@ -26,6 +26,7 @@ class TorrentState(enum.Enum):
     DOWNLOADING = 3
     DOWNLOADED = 4
 
+
 class MediaWebtorrentPlugin(MediaPlugin):
     """
     Plugin to download and stream videos using webtorrent
@@ -118,7 +119,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
         return re.sub('\x1b\[((\d+m)|(.{1,2}))', '', line).strip()
 
 
-    def _process_monitor(self, resource, download_dir):
+    def _process_monitor(self, resource, download_dir, player_type, player_args):
         def _thread():
             if not self._webtorrent_process:
                 return
@@ -162,6 +163,9 @@ class MediaWebtorrentPlugin(MediaPlugin):
                     # Streaming started
                     webtorrent_url = re.search('server running at: (.+?)$',
                                                line, flags=re.IGNORECASE).group(1)
+                    webtorrent_url = webtorrent_url.replace(
+                        'http://localhost', 'http://' + get_ip_or_hostname())
+
                     self.logger.info('Torrent stream started on {}'.format(
                         webtorrent_url))
 
@@ -215,17 +219,19 @@ class MediaWebtorrentPlugin(MediaPlugin):
                 except FileNotFoundError:
                     continue
 
+            player = get_plugin('media.' + player_type) if player_type \
+                else self._media_plugin
+
+            media = media_file if player.is_local() else webtorrent_url
+
             self.logger.info(
                 'Starting playback of {} to {} through {}'.format(
-                    media_file, self._media_plugin.__class__.__name__,
+                    media_file, player.__class__.__name__,
                     webtorrent_url))
 
-            media = media_file if self._media_plugin.is_local() \
-                else webtorrent_url
-
-            self._media_plugin.play(media)
+            player.play(media, **player_args)
             self.logger.info('Waiting for player to terminate')
-            self._wait_for_player()
+            self._wait_for_player(player)
             self.logger.info('Torrent player terminated')
             bus.post(TorrentDownloadCompletedEvent(resource=resource))
 
@@ -235,17 +241,17 @@ class MediaWebtorrentPlugin(MediaPlugin):
 
         return _thread
 
-    def _wait_for_player(self):
-        media_cls = self._media_plugin.__class__.__name__
+    def _wait_for_player(self, player):
+        media_cls = player.__class__.__name__
         stop_evt = None
 
         if media_cls == 'MediaMplayerPlugin':
-            stop_evt = self._media_plugin._mplayer_stopped_event
+            stop_evt = player._mplayer_stopped_event
         elif media_cls == 'MediaOmxplayerPlugin':
             stop_evt = threading.Event()
             def stop_callback():
                 stop_evt.set()
-            self._media_plugin.add_handler('stop', stop_callback)
+            player.add_handler('stop', stop_callback)
 
         if stop_evt:
             stop_evt.wait()
@@ -264,13 +270,22 @@ class MediaWebtorrentPlugin(MediaPlugin):
 
 
     @action
-    def play(self, resource):
+    def play(self, resource, player=None, **player_args):
         """
         Download and stream a torrent
 
         :param resource: Play a resource, as a magnet link, torrent URL or
             torrent file path
         :type resource: str
+
+        :param player: If set, use this plugin type as a player for the
+            torrent. Supported types: 'mplayer', 'omxplayer', 'chromecast'.
+            If not set, then the default configured media plugin will be used.
+        :type player: str
+
+        :param player_args: Any arguments to pass to the player plugin's
+            play() method
+        :type player_args: dict
         """
 
         if self._webtorrent_process:
@@ -291,7 +306,8 @@ class MediaWebtorrentPlugin(MediaPlugin):
                                                     stdout=subprocess.PIPE)
 
         threading.Thread(target=self._process_monitor(
-            resource=resource, download_dir=download_dir)).start()
+            resource=resource, download_dir=download_dir,
+            player_type=player, player_args=player_args)).start()
         return { 'resource': resource }
 
 
diff --git a/platypush/utils/__init__.py b/platypush/utils/__init__.py
index ad9a43af..a5df7bfa 100644
--- a/platypush/utils/__init__.py
+++ b/platypush/utils/__init__.py
@@ -6,6 +6,7 @@ import inspect
 import logging
 import os
 import signal
+import socket
 import ssl
 
 logger = logging.getLogger(__name__)
@@ -210,4 +211,9 @@ def is_process_alive(pid):
         return False
 
 
+def get_ip_or_hostname():
+    ip = socket.gethostbyname(socket.gethostname())
+    return socket.getfqdn() if ip.startswith('127.') else ip
+
+
 # vim:sw=4:ts=4:et: