# -*- 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 self.started = False @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() self.started = True 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: self.started = False 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.started: 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) elif not self.is_alive() and self.started: raise Error("torrent2http has crashed.", Error.CRASHED) 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.started = False self.logpipe = None self.process = None