# -*- coding: utf-8 -*- import os import threading import urllib.error import urllib.parse import urllib.request import sys import time import xbmc import xbmcvfs from . import log as logging from . import FileStatus, SessionStatus from .error import Error from .structs import Encryption from .util import can_bind, find_free_port, localize_path, uri2path, detect_media_type, get_platform platform = get_platform() dirname = os.path.join(xbmcvfs.translatePath('special://temp'), 'xbmcup', 'script.module.gorrent2http') dest_path = os.path.join(dirname, platform['system']) sys.path.insert(0, dest_path) try: from gorrent import gorrent as gt logging.info(f'Imported gorrent v{gt.Version()}') except Exception: import traceback logging.error(f'Error importing gorrent. Exception: {traceback.format_exc()}') raise LOGGING = True class Engine: """ This is python binding class to gorrent2http client. """ def _log(self, message): if self.logger: self.logger(message) else: xbmc.log("[gorrent2http] %s" % message) def __init__(self, uri=None, platform=None, download_path=".", bind_host='127.0.0.1', bind_port=5001, connections_limit=200, download_kbps=-1, upload_kbps=-1, 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='', user_agent=None, startup_timeout=5, state_file='', 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, proxy=None): """ Creates engine instance. It doesn't do anything except initializing object members. For starting engine use start() method. :param uri: Torrent URI (magnet://, file:// or http://) :param platform: Object with two methods implemented: arch() and system() :param download_path: Torrent download path :param bind_host: Bind host of torrent2http :param bind_port: Bind port of torrent2http :param connections_limit: Set a global limit on the number of connections opened :param download_kbps: Max download rate (kB/s) :param upload_kbps: Max upload rate (kB/s) :param enable_dht: Enable DHT (Distributed Hash Table) :param enable_lsd: Enable LSD (Local Service Discovery) :param enable_natpmp: Enable NATPMP (NAT port-mapping) :param enable_upnp: Enable UPnP (UPnP port-mapping) :param enable_scrape: Enable sending scrape request to tracker (updates total peers/seeds count) :param log_stats: Log all stats (incl. log_overall_progress, log_files_progress, log_pieces_progress) :param encryption: Encryption: 0=forced 1=enabled (default) 2=disabled :param keep_complete: Keep complete files after exiting :param keep_incomplete: Keep incomplete files after exiting :param keep_files: Keep all files after exiting (incl. keep_complete and keep_incomplete) :param log_files_progress: Log files progress :param log_overall_progress: Log overall progress :param log_pieces_progress: Log pieces progress :param listen_port: Use specified port for incoming connections :param use_random_port: Use random listen port (49152-65535) :param max_idle_timeout: Automatically shutdown torrent2http if no connection are active after a timeout :param no_sparse: Do not use sparse file allocation :param resume_file: Use fast resume file :param user_agent: Set an user agent :param startup_timeout: torrent2http startup timeout :param state_file: Use file for saving/restoring session state :param enable_utp: Enable uTP protocol :param enable_tcp: Enable TCP protocol :param debug_alerts: Show debug alert notifications :param logger: Instance of logging.Logger :param torrent_connect_boost: The number of peers to try to connect to immediately when the first tracker response is received for a torrent :param connection_speed: The number of peer connection attempts that are made per second :param peer_connect_timeout: The number of seconds to wait after a connection attempt is initiated to a peer :param request_timeout: The number of seconds until the current front piece request will time out :param min_reconnect_time: The time to wait between peer connection attempts. If the peer fails, the time is multiplied by fail counter :param max_failcount: The maximum times we try to connect to a peer before stop connecting again :param dht_routers: List of additional DHT routers (host:port pairs) :param trackers: List of additional tracker URLs """ 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.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.started = False self.proxy = proxy self.message_logger = None self.run_message_logger = True @staticmethod def _validate_save_path(path): path = xbmcvfs.translatePath(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(localize_path(path)): raise Error("Download path doesn't exist (%s)" % path, Error.INVALID_DOWNLOAD_PATH) return localize_path(path) def start(self, start_index): download_path = self._validate_save_path(self.download_path) if not can_bind(self.bind_host, self.bind_port): port = find_free_port(self.bind_host) if port is False: raise Error("Can't find port to bind gorrent2http", 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 self._log("Invoking gorrent2http") class Logging(object): def __init__(self, _log): self._log = _log def info(self, message): if LOGGING: self._log('INFO: %s' % (message,)) def error(self, message): if LOGGING: self._log('ERROR: %s' % (message,)) gt.logging = Logging(self._log) settings = gt.NewSettings() settings.DownloadPath = download_path settings.HttpBindHost = self.bind_host settings.HttpBindPort = self.bind_port settings.ListenPort = self.listen_port settings.TorrentPath = uri2path(self.uri) settings.MaxConnections = self.connections_limit settings.Debug = self.debug_alerts settings.KeepFiles = self.keep_files settings.Proxy = len(self.proxy) > 0 and f'socks5://{self.proxy["host"]}:{self.proxy["port"]}' or '' self.engine = gt.NewEngine(settings) def msg_logger(): while self.run_message_logger: msg = self.engine.GetMsg() if msg not in ('__NO_MSG__', '__CLOSED__'): xbmc.log(f'-= GORRENT =-: {msg}') time.sleep(0.1) self.message_logger = threading.Thread(target=msg_logger) self.message_logger.start() self._log('starting torrent') self.engine.StartTorrent(start_index) self._log('waiting alive status set') 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 gorrent2http, 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 gorrent2http, time is out", Error.TIMEOUT) self._log("gorrent2http successfully started.") def pause(self): pass def resume(self): pass 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): stat = self.engine.Status() stat_kw = { 'download_rate': stat.DownloadRate, 'upload_rate': stat.UploadRate, 'num_seeds': stat.Seeds, 'name': '', 'state': 0, 'state_str': '', 'error': '', 'progress': 0, 'total_download': 0, 'total_upload': 0, 'num_peers': 0, 'total_seeds': 0, 'total_peers': 0 } return SessionStatus(**stat_kw) def list_from_info(self, media_types=None): try: info = gt.GetMetaFromFile(uri2path(self.uri)) except: import traceback xbmc.log(f'info load exception: {traceback.format_exc()}') return [] files = [] if len(info.Files) > 0: for i in range(len(info.Files)): f = info.Files[i] uri = 'http://' + "%s:%s" % (self.bind_host, self.bind_port) + '/files/' + urllib.parse.quote( '/'.join(f.Path)) files.append({ 'name': localize_path('/'.join(f.Path)), 'size': f.Length, 'offset': i, 'media_type': media_types is not None and detect_media_type(f.Path[-1]) or '', 'download': 0, 'progress': 0.0, 'save_path': '', 'url': uri }) else: files.append({ 'name': localize_path(info.Name), 'size': info.Length, 'offset': 0, 'media_type': media_types is not None and detect_media_type(info.Name) or '', 'download': 0, 'progress': 0.0, 'save_path': '', 'url': 'http://' + "%s:%s" % (self.bind_host, self.bind_port) + '/files/' + urllib.parse.quote( info.Name) }) res = [] if len(files) > 0: res = [FileStatus(index=index, **f) for index, f in enumerate(files)] if media_types is not None: res = [fs for fs in res if fs.media_type in media_types] return res def file_status(self, file_index, timeout=10): try: efs = self.engine.FileStatus(file_index) fstat = { 'name': localize_path('/'.join(efs.Name)), 'progress': efs.Progress, 'url': efs.Url, 'save_path': '', 'size': efs.Length, 'offset': 0, 'download': 0, 'media_type': '' } return FileStatus(index=file_index, **fstat) except: raise Error("Requested file index (%d) is invalid" % (file_index,), Error.INVALID_FILE_INDEX, file_index=file_index) def is_alive(self): return self.engine.IsAlive() def close(self): if self.is_alive(): self._log("Shutting down gorrent2http...") self.engine.Stop() finished = False self.wait_on_close_timeout = 10 if self.wait_on_close_timeout is not None: start = time.time() 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("PANIC: Timeout occurred while shutting down gorrent2http thread") else: self._log("gorrent2http successfully shut down.") self.wait_on_close_timeout = None self._log("gorrent2http successfully shut down.") if self.message_logger is not None and self.message_logger.is_alive(): self.run_message_logger = False self.message_logger = None self.started = False