script.module.pyrrent2http/lib/torrent2http/engine.py

368 lines
16 KiB
Python

# -*- coding: utf-8 -*-
import json
import os
import socket
import stat
import subprocess
import sys
import time
import urllib2
import logpipe
import mimetypes
import xbmc
from error import Error
from platform import Platform
from . import SessionStatus, FileStatus, PeerInfo, MediaType, Encryption
from os.path import dirname
class Engine:
SUBTITLES_FORMATS = ['.aqt', '.gsub', '.jss', '.sub', '.ttxt', '.pjs', '.psb', '.rt', '.smi', '.stl',
'.ssf', '.srt', '.ssa', '.ass', '.usf', '.idx']
def _ensure_binary_executable(self, path):
st = os.stat(path)
if not st.st_mode & stat.S_IEXEC:
self._log("%s is not executable, trying to change its mode..." % path)
os.chmod(path, st.st_mode | stat.S_IEXEC)
st = os.stat(path)
if st.st_mode & stat.S_IEXEC:
self._log("Succeeded")
return True
else:
self._log("Failed")
return False
return True
def _log(self, message):
if self.logger:
self.logger(message)
else:
xbmc.log("[torrent2http] %s" % message)
def _get_binary_path(self, binaries_path):
binary = "torrent2http" + (".exe" if self.platform.system == 'windows' else "")
binary_dir = os.path.join(binaries_path, "%s_%s" % (self.platform.system, self.platform.arch))
binary_path = os.path.join(binary_dir, binary)
if not os.path.isfile(binary_path):
raise Error("Can't find torrent2http binary for %s" % self.platform,
Error.UNKNOWN_PLATFORM, platform=str(self.platform))
if not self._ensure_binary_executable(binary_path):
if self.platform.system == "android":
self._log("Trying to copy torrent2http to ext4, since the sdcard is noexec...")
xbmc_home = os.environ.get('XBMC_HOME') or os.environ.get('KODI_HOME')
if not xbmc_home:
raise Error("Suppose we are running XBMC, but environment variable "
"XBMC_HOME or KODI_HOME is not found", Error.XBMC_HOME_NOT_DEFINED)
base_xbmc_path = dirname(dirname(dirname(xbmc_home)))
android_binary_dir = os.path.join(base_xbmc_path, "files")
if not os.path.exists(android_binary_dir):
os.makedirs(android_binary_dir)
android_binary_path = os.path.join(android_binary_dir, binary)
if not os.path.exists(android_binary_path) or \
int(os.path.getmtime(android_binary_path)) < int(os.path.getmtime(binary_path)):
import shutil
shutil.copy2(binary_path, android_binary_path)
if not self._ensure_binary_executable(android_binary_path):
raise Error("Can't make %s executable" % android_binary_path, Error.NOEXEC_FILESYSTEM)
binary_path = android_binary_path
else:
raise Error("Can't make %s executable, ensure it's placed on exec partition and "
"partition is in read/write mode" % binary_path, Error.NOEXEC_FILESYSTEM)
self._log("Selected %s as torrent2http binary" % binary_path)
return binary_path
@staticmethod
def _can_bind(host, port):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host, port))
s.close()
except socket.error:
return False
return True
@staticmethod
def _find_free_port(host):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host, 0))
port = s.getsockname()[1]
s.close()
except socket.error:
return False
return port
def __init__(self, uri=None, binaries_path=None, platform=None, download_path=".",
bind_host='127.0.0.1', bind_port=5001, connections_limit=None, download_kbps=None, upload_kbps=None,
enable_dht=True, enable_lsd=True, enable_natpmp=True, enable_upnp=True, enable_scrape=False,
log_stats=False, encryption=Encryption.ENABLED, keep_complete=False, keep_incomplete=False,
keep_files=False, log_files_progress=False, log_overall_progress=False, log_pieces_progress=False,
listen_port=6881, use_random_port=False, max_idle_timeout=None, no_sparse=False, resume_file=None,
user_agent=None, startup_timeout=5, state_file=None, enable_utp=True, enable_tcp=True,
debug_alerts=False, logger=None, torrent_connect_boost=50, connection_speed=50,
peer_connect_timeout=15, request_timeout=20, min_reconnect_time=60, max_failcount=3,
dht_routers=None, trackers=None):
self.dht_routers = dht_routers or []
self.trackers = trackers or []
self.max_failcount = max_failcount
self.min_reconnect_time = min_reconnect_time
self.request_timeout = request_timeout
self.peer_connect_timeout = peer_connect_timeout
self.connection_speed = connection_speed
self.torrent_connect_boost = torrent_connect_boost
self.platform = platform
self.bind_host = bind_host
self.bind_port = bind_port
self.binaries_path = binaries_path or os.path.join(dirname(dirname(dirname(os.path.abspath(__file__)))), 'bin')
self.download_path = download_path
self.connections_limit = connections_limit
self.download_kbps = download_kbps
self.upload_kbps = upload_kbps
self.enable_dht = enable_dht
self.enable_lsd = enable_lsd
self.enable_natpmp = enable_natpmp
self.enable_upnp = enable_upnp
self.enable_scrape = enable_scrape
self.log_stats = log_stats
self.encryption = encryption
self.keep_complete = keep_complete
self.keep_incomplete = keep_incomplete
self.keep_files = keep_files
self.log_files_progress = log_files_progress
self.log_overall_progress = log_overall_progress
self.log_pieces_progress = log_pieces_progress
self.listen_port = listen_port
self.use_random_port = use_random_port
self.max_idle_timeout = max_idle_timeout
self.no_sparse = no_sparse
self.resume_file = resume_file
self.user_agent = user_agent
self.startup_timeout = startup_timeout
self.state_file = state_file
self.wait_on_close_timeout = None
self.enable_utp = enable_utp
self.enable_tcp = enable_tcp
self.debug_alerts = debug_alerts
self.logger = logger
self.uri = uri
self.logpipe = None
self.process = None
@staticmethod
def _validate_save_path(path):
if "://" in path:
if sys.platform.startswith('win') and path.lower().startswith("smb://"):
path = path.replace("smb:", "").replace("/", "\\")
else:
raise Error("Downloading to an unmounted network share is not supported", Error.INVALID_DOWNLOAD_PATH)
if not os.path.isdir(path):
raise Error("Download path doesn't exist (%s)" % path, Error.INVALID_DOWNLOAD_PATH)
return path
def start(self, start_index=None):
self.platform = self.platform or Platform()
binary_path = self._get_binary_path(self.binaries_path)
download_path = self._validate_save_path(self.download_path)
if not self._can_bind(self.bind_host, self.bind_port):
port = self._find_free_port(self.bind_host)
if port is False:
raise Error("Can't find port to bind torrent2http", Error.BIND_ERROR)
self._log("Can't bind to %s:%s, so we found another port: %d" % (self.bind_host, self.bind_port, port))
self.bind_port = port
kwargs = {
'--bind': "%s:%s" % (self.bind_host, self.bind_port),
'--uri': self.uri,
'--file-index': start_index,
'--dl-path': download_path,
'--connections-limit': self.connections_limit,
'--dl-rate': self.download_kbps,
'--ul-rate': self.upload_kbps,
'--enable-dht': self.enable_dht,
'--enable-lsd': self.enable_lsd,
'--enable-natpmp': self.enable_natpmp,
'--enable-upnp': self.enable_upnp,
'--enable-scrape': self.enable_scrape,
'--encryption': self.encryption,
'--show-stats': self.log_stats,
'--files-progress': self.log_files_progress,
'--overall-progress': self.log_overall_progress,
'--pieces-progress': self.log_pieces_progress,
'--listen-port': self.listen_port,
'--random-port': self.use_random_port,
'--keep-complete': self.keep_complete,
'--keep-incomplete': self.keep_incomplete,
'--keep-files': self.keep_files,
'--max-idle': self.max_idle_timeout,
'--no-sparse': self.no_sparse,
'--resume-file': self.resume_file,
'--user-agent': self.user_agent,
'--state-file': self.state_file,
'--enable-utp': self.enable_utp,
'--enable-tcp': self.enable_tcp,
'--debug-alerts': self.debug_alerts,
'--torrent-connect-boost': self.torrent_connect_boost,
'--connection-speed': self.connection_speed,
'--peer-connect-timeout': self.peer_connect_timeout,
'--request-timeout': self.request_timeout,
'--min-reconnect-time': self.min_reconnect_time,
'--max-failcount': self.max_failcount,
'--dht-routers': ",".join(self.dht_routers),
'--trackers': ",".join(self.trackers),
}
args = [binary_path]
for k, v in kwargs.iteritems():
if v is not None:
if isinstance(v, bool):
if v:
args.append(k)
else:
args.append("%s=false" % k)
else:
args.append(k)
if isinstance(v, str):
v = v.decode('utf-8')
if isinstance(v, unicode):
v = v.encode(sys.getfilesystemencoding() or 'utf-8')
else:
v = str(v)
args.append(v)
self._log("Invoking %s" % " ".join(args))
startupinfo = None
if self.platform.system == "windows":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= 1
startupinfo.wShowWindow = 0
self.logpipe = logpipe.LogPipe(self._log)
try:
self.process = subprocess.Popen(args, stderr=self.logpipe, stdout=self.logpipe, startupinfo=startupinfo)
except OSError, e:
raise Error("Can't start torrent2http: %r" % e, Error.POPEN_ERROR)
start = time.time()
initialized = False
while (time.time() - start) < self.startup_timeout:
time.sleep(0.1)
if not self.is_alive():
raise Error("Can't start torrent2http, see log for details", Error.PROCESS_ERROR)
try:
self.status(1)
initialized = True
break
except Error:
pass
if not initialized:
raise Error("Can't start torrent2http, time is out", Error.TIMEOUT)
self._log("torrent2http successfully started.")
def check_torrent_error(self, status=None):
if not status:
status = self.status()
if status.error:
raise Error("Torrent error: %s" % status.error, Error.TORRENT_ERROR, reason=status.error)
def status(self, timeout=10):
status = self._decode(self._request('status', timeout))
status = SessionStatus(**status)
return status
def _detect_media_type(self, name):
ext = os.path.splitext(name)[1]
if ext in self.SUBTITLES_FORMATS:
return MediaType.SUBTITLES
else:
mime_type = mimetypes.guess_type(name)[0]
if not mime_type:
return MediaType.UNKNOWN
mime_type = mime_type.split("/")[0]
if mime_type == 'audio':
return MediaType.AUDIO
elif mime_type == 'video':
return MediaType.VIDEO
else:
return MediaType.UNKNOWN
def list(self, media_types=None, timeout=10):
files = self._decode(self._request('ls', timeout))['files']
if files:
res = [FileStatus(index=index, media_type=self._detect_media_type(f['name']), **f)
for index, f in enumerate(files)]
if media_types is not None:
res = filter(lambda fs: fs.media_type in media_types, res)
return res
def file_status(self, file_index, timeout=10):
res = self.list(timeout=timeout)
if res:
try:
return next((f for f in res if f.index == file_index))
except StopIteration:
raise Error("Requested file index (%d) is invalid" % file_index, Error.INVALID_FILE_INDEX,
file_index=file_index)
def peers(self, timeout=10):
peers = self._decode(self._request('peers', timeout))['peers']
if peers:
return [PeerInfo(**p) for p in peers]
def is_alive(self):
return self.process and self.process.poll() is None
@staticmethod
def _decode(response):
try:
return json.loads(response)
except (KeyError, ValueError), e:
raise Error("Can't decode response from torrent2http: %r" % e, Error.REQUEST_ERROR)
def _request(self, cmd, timeout=None):
if not self.is_alive():
raise Error("torrent2http is not started", Error.REQUEST_ERROR)
try:
url = "http://%s:%s/%s" % (self.bind_host, self.bind_port, cmd)
kwargs = {}
if timeout is not None:
kwargs['timeout'] = timeout
return urllib2.urlopen(url, **kwargs).read()
except urllib2.URLError as e:
if isinstance(e.reason, socket.timeout):
raise Error("Timeout occurred while sending command '%s' to torrent2http" % cmd, Error.TIMEOUT)
else:
raise Error("Can't send command '%s' to torrent2http: %r" % (cmd, e), Error.REQUEST_ERROR)
except socket.error as e:
reason = e[1] if isinstance(e, tuple) else e
raise Error("Can't read from torrent2http: %s" % reason, Error.REQUEST_ERROR)
def wait_on_close(self, wait_timeout=10):
self.wait_on_close_timeout = wait_timeout
def close(self):
if self.logpipe and self.wait_on_close_timeout is None:
self.logpipe.close()
if self.is_alive():
self._log("Shutting down torrent2http...")
self._request('shutdown')
finished = False
if self.wait_on_close_timeout is not None:
start = time.time()
os.close(self.logpipe.write_fd)
while (time.time() - start) < self.wait_on_close_timeout:
time.sleep(0.5)
if not self.is_alive():
finished = True
break
if not finished:
self._log("Timeout occurred while shutting down torrent2http, killing it")
self.process.kill()
else:
self._log("torrent2http successfully shut down.")
self.wait_on_close_timeout = None
self.logpipe = None
self.process = None