357 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			357 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# -*- 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
 |