2019-02-04 01:01:39 +01:00
|
|
|
import datetime
|
2019-02-03 17:43:30 +01:00
|
|
|
import os
|
|
|
|
import select
|
|
|
|
import subprocess
|
2019-02-04 01:01:39 +01:00
|
|
|
import tempfile
|
2019-02-03 17:43:30 +01:00
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
from platypush.config import Config
|
2019-02-03 17:43:30 +01:00
|
|
|
from platypush.context import get_bus, get_plugin
|
|
|
|
from platypush.message.response import Response
|
|
|
|
from platypush.plugins.media import PlayerState, MediaPlugin
|
2019-02-04 01:01:39 +01:00
|
|
|
from platypush.message.event.torrent import TorrentDownloadStartEvent, \
|
|
|
|
TorrentDownloadCompletedEvent
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
from platypush.plugins import action
|
|
|
|
from platypush.utils import find_bins_in_path
|
|
|
|
|
|
|
|
|
|
|
|
class MediaWebtorrentPlugin(MediaPlugin):
|
|
|
|
"""
|
|
|
|
Plugin to download and stream videos using webtorrent
|
|
|
|
|
|
|
|
Requires:
|
|
|
|
|
|
|
|
* **webtorrent** installed on your system (``npm install -g webtorrent``)
|
|
|
|
* **webtorrent-cli** installed on your system
|
|
|
|
(``npm install -g webtorrent-cli`` or better
|
|
|
|
``npm install -g BlackLight/webtorrent-cli`` as my fork contains
|
|
|
|
the ``--[player]-args`` options to pass custom arguments to your
|
|
|
|
installed player)
|
2019-02-04 01:01:39 +01:00
|
|
|
* A media plugin configured for streaming (e.g. media.mplayer
|
|
|
|
or media.omxplayer)
|
2019-02-03 17:43:30 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
_supported_media_plugins = { 'media.mplayer', 'media.omxplayer' }
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
# Download at least 10 MBs before starting streaming
|
|
|
|
_download_size_before_streaming = 10 * 2**20
|
2019-02-03 17:43:30 +01:00
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
def __init__(self, webtorrent_bin=None, *args, **kwargs):
|
2019-02-03 17:43:30 +01:00
|
|
|
"""
|
2019-02-04 01:01:39 +01:00
|
|
|
media.webtorrent will use the default media player plugin you have
|
|
|
|
configured (e.g. mplayer, omxplayer) to stream the torrent.
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
:param webtorrent_bin: Path to your webtorrent executable. If not set,
|
|
|
|
then Platypush will search for the right executable in your PATH
|
|
|
|
:type webtorrent_bin: str
|
|
|
|
"""
|
|
|
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
self._init_webtorrent_bin(webtorrent_bin=webtorrent_bin)
|
2019-02-04 01:01:39 +01:00
|
|
|
self._init_media_player()
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _init_webtorrent_bin(self, webtorrent_bin=None):
|
2019-02-04 01:01:39 +01:00
|
|
|
self._webtorrent_process = None
|
|
|
|
|
2019-02-03 17:43:30 +01:00
|
|
|
if not webtorrent_bin:
|
|
|
|
bin_name = 'webtorrent.exe' if os.name == 'nt' else 'webtorrent'
|
|
|
|
bins = find_bins_in_path(bin_name)
|
|
|
|
|
|
|
|
if not bins:
|
|
|
|
raise RuntimeError('Webtorrent executable not specified and ' +
|
|
|
|
'not found in your PATH. Make sure that ' +
|
|
|
|
'webtorrent is either installed or ' +
|
|
|
|
'configured and that both webtorrent and ' +
|
|
|
|
'webtorrent-cli are installed')
|
|
|
|
|
|
|
|
self.webtorrent_bin = bins[0]
|
|
|
|
else:
|
|
|
|
webtorrent_bin = os.path.expanduser(webtorrent_bin)
|
|
|
|
if not (os.path.isfile(webtorrent_bin)
|
|
|
|
and (os.name == 'nt' or os.access(webtorrent_bin, os.X_OK))):
|
|
|
|
raise RuntimeError('{} is does not exist or is not a valid ' +
|
|
|
|
'executable file'.format(webtorrent_bin))
|
|
|
|
|
|
|
|
self.webtorrent_bin = webtorrent_bin
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
def _init_media_player(self):
|
|
|
|
self._media_plugin = None
|
|
|
|
plugin_name = None
|
2019-02-03 17:43:30 +01:00
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
for plugin_name in self._supported_media_plugins:
|
|
|
|
try:
|
|
|
|
if Config.get(plugin_name):
|
|
|
|
self._media_plugin = get_plugin(plugin_name)
|
2019-02-03 17:43:30 +01:00
|
|
|
break
|
2019-02-04 01:01:39 +01:00
|
|
|
except:
|
|
|
|
pass
|
2019-02-03 17:43:30 +01:00
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
if not self._media_plugin:
|
|
|
|
raise RuntimeError(('No media player specified and no ' +
|
|
|
|
'compatible media plugin configured - ' +
|
|
|
|
'supported media plugins: {}').format(
|
|
|
|
self._supported_media_plugins))
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
def _process_monitor(self, resource, output_file):
|
|
|
|
def _thread():
|
|
|
|
if not self._webtorrent_process:
|
|
|
|
return
|
|
|
|
|
|
|
|
bus = get_bus()
|
|
|
|
bus.post(TorrentDownloadStartEvent(resource=resource))
|
2019-02-03 17:43:30 +01:00
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
while True:
|
2019-02-03 17:43:30 +01:00
|
|
|
try:
|
2019-02-04 01:01:39 +01:00
|
|
|
if os.path.getsize(output_file) > \
|
|
|
|
self._download_size_before_streaming:
|
|
|
|
break
|
|
|
|
except FileNotFoundError:
|
2019-02-03 17:43:30 +01:00
|
|
|
pass
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
self._media_plugin.play(output_file)
|
|
|
|
self._webtorrent_process.wait()
|
|
|
|
bus.post(TorrentDownloadCompletedEvent(resource=resource))
|
|
|
|
self._webtorrent_process = None
|
2019-02-03 17:43:30 +01:00
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
return _thread
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
def _get_torrent_download_path(self):
|
|
|
|
if self._media_plugin.download_dir:
|
|
|
|
# TODO set proper file name based on the torrent metadata
|
|
|
|
return os.path.join(self._media_plugin.download_dir,
|
|
|
|
'torrent_media_' + datetime.datetime.
|
|
|
|
today().strftime('%Y-%m-%d_%H-%M-%S-%f'))
|
2019-02-03 17:43:30 +01:00
|
|
|
else:
|
2019-02-04 01:01:39 +01:00
|
|
|
return tempfile.NamedTemporaryFile(delete=False).name
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
@action
|
|
|
|
def play(self, resource):
|
|
|
|
"""
|
|
|
|
Download and stream a torrent
|
|
|
|
|
|
|
|
:param resource: Play a resource, as a magnet link, torrent URL or
|
|
|
|
torrent file path
|
|
|
|
:type resource: str
|
|
|
|
"""
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
if self._webtorrent_process:
|
|
|
|
try:
|
|
|
|
self.quit()
|
|
|
|
except:
|
|
|
|
self.logger.debug('Failed to quit the previous instance: {}'.
|
|
|
|
format(str))
|
|
|
|
|
|
|
|
output_file = self._get_torrent_download_path()
|
|
|
|
webtorrent_args = [self.webtorrent_bin, '--stdout', resource]
|
|
|
|
|
|
|
|
with open(output_file, 'w') as f:
|
|
|
|
self._webtorrent_process = subprocess.Popen(webtorrent_args,
|
|
|
|
stdout=f)
|
|
|
|
|
|
|
|
threading.Thread(target=self._process_monitor(
|
|
|
|
resource=resource, output_file=output_file)).start()
|
|
|
|
|
|
|
|
return { 'resource': resource }
|
|
|
|
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def stop(self):
|
|
|
|
""" Stop the playback """
|
|
|
|
return self.quit()
|
|
|
|
|
|
|
|
@action
|
|
|
|
def quit(self):
|
|
|
|
""" Quit the player """
|
2019-02-04 01:01:39 +01:00
|
|
|
if self._webtorrent_process and self._is_process_alive(
|
|
|
|
self._webtorrent_process.pid):
|
|
|
|
self._webtorrent_process.terminate()
|
|
|
|
self._webtorrent_process.wait()
|
|
|
|
try: self._webtorrent_process.kill()
|
2019-02-03 17:43:30 +01:00
|
|
|
except: pass
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
self._webtorrent_process = None
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def load(self, resource):
|
|
|
|
"""
|
|
|
|
Load a torrent resource in the player.
|
|
|
|
"""
|
|
|
|
return self.play(resource)
|
|
|
|
|
|
|
|
def _is_process_alive(self):
|
2019-02-04 01:01:39 +01:00
|
|
|
if not self._webtorrent_process:
|
2019-02-03 17:43:30 +01:00
|
|
|
return False
|
|
|
|
|
|
|
|
try:
|
2019-02-04 01:01:39 +01:00
|
|
|
os.kill(self._webtorrent_process.pid, 0)
|
2019-02-03 17:43:30 +01:00
|
|
|
return True
|
|
|
|
except OSError:
|
2019-02-04 01:01:39 +01:00
|
|
|
self._webtorrent_process = None
|
2019-02-03 17:43:30 +01:00
|
|
|
return False
|
|
|
|
|
|
|
|
@action
|
|
|
|
def status(self):
|
|
|
|
"""
|
|
|
|
Get the current player state.
|
|
|
|
|
|
|
|
:returns: A dictionary containing the current state.
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
output = {
|
|
|
|
"state": "play" # or "stop" or "pause"
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
|
2019-02-04 01:01:39 +01:00
|
|
|
return {'state': self._media_plugin.status()
|
|
|
|
.get('state', PlayerState.STOP.value)}
|
2019-02-03 17:43:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|