Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
Роман Бородин | a4d535d67f | |
Роман Бородин | 6e64e0a1f4 | |
Роман Бородин | bf655e3f96 | |
Роман Бородин | b1da58c3b3 | |
Роман Бородин | ca2a815140 |
|
@ -1,8 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="script.module.pyrrent2http" name="pyrrent2http" version="1.1.1" provider-name="inpos">
|
<addon id="script.module.gorrent2http" name="gorrent2http" version="1.2.3" provider-name="inpos">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="3.0.0"/>
|
<import addon="xbmc.python" version="3.0.0"/>
|
||||||
<import addon="script.module.libtorrent" version="1.2.0"/>
|
|
||||||
<import addon="script.module.chardet" />
|
<import addon="script.module.chardet" />
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.module" library="lib"/>
|
<extension point="xbmc.python.module" library="lib"/>
|
||||||
|
@ -17,5 +16,5 @@
|
||||||
<description lang="ru">Обеспечивает последовательную (sequential) загрузку торрентов для потокового онлайн просмотра через HTTP. Основан на библиотеке LibTorrent.</description>
|
<description lang="ru">Обеспечивает последовательную (sequential) загрузку торрентов для потокового онлайн просмотра через HTTP. Основан на библиотеке LibTorrent.</description>
|
||||||
<description lang="en">Provides sequential torrent downloading for online streaming video and other media over HTTP.</description>
|
<description lang="en">Provides sequential torrent downloading for online streaming video and other media over HTTP.</description>
|
||||||
<email>inpos@yandex.ru</email>
|
<email>inpos@yandex.ru</email>
|
||||||
<source>https://git.ukamnya.ru/ukamnya/script.module.pyrrent2http</source></extension>
|
<source>https://git.ukamnya.ru/ukamnya/script.module.gorrent2http</source></extension>
|
||||||
</addon>
|
</addon>
|
||||||
|
|
|
@ -0,0 +1,285 @@
|
||||||
|
# -*- 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, download_path=".",
|
||||||
|
bind_host='127.0.0.1', bind_port=5001, connections_limit=200,
|
||||||
|
keep_files=False,
|
||||||
|
listen_port=6881,
|
||||||
|
debug_alerts=False,
|
||||||
|
proxy=None,
|
||||||
|
seed=True,
|
||||||
|
accept_peer_conn=True,
|
||||||
|
startup_timeout=5,
|
||||||
|
logger=None,
|
||||||
|
):
|
||||||
|
|
||||||
|
self.platform = platform
|
||||||
|
self.bind_host = bind_host
|
||||||
|
self.bind_port = bind_port
|
||||||
|
self.download_path = download_path
|
||||||
|
self.connections_limit = connections_limit
|
||||||
|
self.keep_files = keep_files
|
||||||
|
self.listen_port = listen_port
|
||||||
|
self.wait_on_close_timeout = None
|
||||||
|
self.debug_alerts = debug_alerts
|
||||||
|
self.uri = uri
|
||||||
|
self.started = False
|
||||||
|
self.proxy = proxy
|
||||||
|
self.seed = seed
|
||||||
|
self.accept_peer_conn = accept_peer_conn
|
||||||
|
self.startup_timeout = startup_timeout
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
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 ''
|
||||||
|
settings.Seed = self.seed
|
||||||
|
settings.AcceptPeerConnections = self.accept_peer_conn
|
||||||
|
|
||||||
|
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
|
|
@ -1,30 +1,38 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
|
||||||
import chardet
|
|
||||||
|
|
||||||
try:
|
|
||||||
from python_libtorrent import get_libtorrent # @UnresolvedImport
|
|
||||||
|
|
||||||
lt = get_libtorrent()
|
|
||||||
print(('Imported libtorrent v%s from python_libtorrent' % (lt.version,)))
|
|
||||||
except Exception as e:
|
|
||||||
print(('Error importing python_libtorrent.Exception: %s' % (str(e),)))
|
|
||||||
try:
|
|
||||||
import libtorrent as lt # @UnresolvedImport
|
|
||||||
except Exception as e:
|
|
||||||
strerror = e.args
|
|
||||||
print(strerror)
|
|
||||||
raise
|
|
||||||
|
|
||||||
from random import SystemRandom
|
|
||||||
import time
|
|
||||||
import urllib.request, urllib.parse, urllib.error
|
|
||||||
import http.server
|
import http.server
|
||||||
|
import io
|
||||||
|
import os
|
||||||
import socketserver
|
import socketserver
|
||||||
import threading
|
import threading
|
||||||
import io
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from random import SystemRandom
|
||||||
|
from . import log as logging
|
||||||
|
|
||||||
|
import chardet
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import xbmc
|
||||||
|
import xbmcvfs
|
||||||
|
|
||||||
|
from . import util
|
||||||
from .util import localize_path, Struct, detect_media_type, uri2path, encode_msg
|
from .util import localize_path, Struct, detect_media_type, uri2path, encode_msg
|
||||||
|
|
||||||
|
|
||||||
|
platform = util.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
|
||||||
|
|
||||||
if os.getenv('ANDROID_ROOT'):
|
if os.getenv('ANDROID_ROOT'):
|
||||||
from ctypes import *
|
from ctypes import *
|
||||||
|
|
||||||
|
@ -72,9 +80,9 @@ if not hasattr(os, 'getppid'):
|
||||||
|
|
||||||
|
|
||||||
def getppid():
|
def getppid():
|
||||||
'''
|
"""
|
||||||
:return: The pid of the parent of this process.
|
:return: The pid of the parent of this process.
|
||||||
'''
|
"""
|
||||||
pe = PROCESSENTRY32()
|
pe = PROCESSENTRY32()
|
||||||
pe.dwSize = ctypes.sizeof(PROCESSENTRY32)
|
pe.dwSize = ctypes.sizeof(PROCESSENTRY32)
|
||||||
mypid = GetCurrentProcessId()
|
mypid = GetCurrentProcessId()
|
||||||
|
@ -162,7 +170,8 @@ class TorrentFile(object):
|
||||||
self.tfs = tfs
|
self.tfs = tfs
|
||||||
self.fileEntry = fileEntry
|
self.fileEntry = fileEntry
|
||||||
self.name = self.fileEntry.path
|
self.name = self.fileEntry.path
|
||||||
self.unicode_name = isinstance(self.name, str) and self.name or self.name.decode(chardet.detect(self.name)['encoding'])
|
self.unicode_name = isinstance(self.name, str) and self.name or self.name.decode(
|
||||||
|
chardet.detect(self.name)['encoding'])
|
||||||
self.media_type = detect_media_type(self.unicode_name)
|
self.media_type = detect_media_type(self.unicode_name)
|
||||||
self.save_path = savePath
|
self.save_path = savePath
|
||||||
self.index = index
|
self.index = index
|
||||||
|
@ -183,7 +192,7 @@ class TorrentFile(object):
|
||||||
return None
|
return None
|
||||||
if self.filePtr is None:
|
if self.filePtr is None:
|
||||||
while not os.path.exists(self.save_path):
|
while not os.path.exists(self.save_path):
|
||||||
logging.info('Waiting for file: %s' % (self.save_path,))
|
xbmc.log('INFO: Waiting for file: %s' % (self.save_path,))
|
||||||
self.tfs.handle.flush_cache()
|
self.tfs.handle.flush_cache()
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
if os.getenv('ANDROID_ROOT'):
|
if os.getenv('ANDROID_ROOT'):
|
||||||
|
@ -194,7 +203,7 @@ class TorrentFile(object):
|
||||||
|
|
||||||
def log(self, message):
|
def log(self, message):
|
||||||
fnum = self.tfs.openedFiles.index(self)
|
fnum = self.tfs.openedFiles.index(self)
|
||||||
logging.info("[Thread No.%d] %s\n" % (fnum, message))
|
xbmc.log("INFO: [Thread No.%d] %s\n" % (fnum, message))
|
||||||
|
|
||||||
def Pieces(self):
|
def Pieces(self):
|
||||||
startPiece, _ = self.pieceFromOffset(1)
|
startPiece, _ = self.pieceFromOffset(1)
|
||||||
|
@ -545,10 +554,10 @@ def HttpHandlerFactory():
|
||||||
return HttpHandler
|
return HttpHandler
|
||||||
|
|
||||||
|
|
||||||
class Pyrrent2http(object):
|
class Gorrent2http(object):
|
||||||
pause = False
|
pause = False
|
||||||
|
|
||||||
def __init__(self, uri='', bindAddress='localhost:5001', downloadPath='.',
|
def __init__(self, uri='', bind_address='localhost:5001', download_path='.',
|
||||||
idleTimeout=-1, keepComplete=False,
|
idleTimeout=-1, keepComplete=False,
|
||||||
keepIncomplete=False, keepFiles=False, showAllStats=False,
|
keepIncomplete=False, keepFiles=False, showAllStats=False,
|
||||||
showOverallProgress=False, showFilesProgress=False,
|
showOverallProgress=False, showFilesProgress=False,
|
||||||
|
@ -568,8 +577,8 @@ class Pyrrent2http(object):
|
||||||
|
|
||||||
self.config = Struct()
|
self.config = Struct()
|
||||||
self.config.uri = uri
|
self.config.uri = uri
|
||||||
self.config.bindAddress = bindAddress
|
self.config.bindAddress = bind_address
|
||||||
self.config.downloadPath = downloadPath
|
self.config.downloadPath = download_path
|
||||||
self.config.idleTimeout = idleTimeout
|
self.config.idleTimeout = idleTimeout
|
||||||
self.config.keepComplete = keepComplete
|
self.config.keepComplete = keepComplete
|
||||||
self.config.keepIncomplete = keepIncomplete
|
self.config.keepIncomplete = keepIncomplete
|
||||||
|
@ -618,7 +627,7 @@ class Pyrrent2http(object):
|
||||||
try:
|
try:
|
||||||
absPath = uri2path(uri)
|
absPath = uri2path(uri)
|
||||||
logging.info('Opening local torrent file: %s' % (encode_msg(absPath),))
|
logging.info('Opening local torrent file: %s' % (encode_msg(absPath),))
|
||||||
torrent_info = lt.torrent_info(lt.bdecode(open(absPath, 'rb').read()))
|
torrent_info = gt.torrent_info(gt.bdecode(open(absPath, 'rb').read()))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
strerror = e.args
|
strerror = e.args
|
||||||
logging.error('Build torrent params error is (%s)' % (strerror,))
|
logging.error('Build torrent params error is (%s)' % (strerror,))
|
||||||
|
@ -639,7 +648,7 @@ class Pyrrent2http(object):
|
||||||
logging.error(strerror)
|
logging.error(strerror)
|
||||||
if self.config.noSparseFile or self.magnet:
|
if self.config.noSparseFile or self.magnet:
|
||||||
logging.info('Disabling sparse file support...')
|
logging.info('Disabling sparse file support...')
|
||||||
torrentParams["storage_mode"] = lt.storage_mode_t.storage_mode_allocate
|
torrentParams["storage_mode"] = gt.storage_mode_t.storage_mode_allocate
|
||||||
return torrentParams
|
return torrentParams
|
||||||
|
|
||||||
def addTorrent(self):
|
def addTorrent(self):
|
||||||
|
@ -704,14 +713,14 @@ class Pyrrent2http(object):
|
||||||
|
|
||||||
def startSession(self):
|
def startSession(self):
|
||||||
logging.info('Starting session...')
|
logging.info('Starting session...')
|
||||||
self.session = lt.session(lt.fingerprint('LT', lt.version_major, lt.version_minor, 0, 0),
|
self.session = gt.session(gt.fingerprint('LT', gt.version_major, gt.version_minor, 0, 0),
|
||||||
flags=int(lt.session_flags_t.add_default_plugins))
|
flags=int(gt.session_flags_t.add_default_plugins))
|
||||||
alertMask = (lt.alert.category_t.error_notification |
|
alertMask = (gt.alert.category_t.error_notification |
|
||||||
lt.alert.category_t.storage_notification |
|
gt.alert.category_t.storage_notification |
|
||||||
lt.alert.category_t.tracker_notification |
|
gt.alert.category_t.tracker_notification |
|
||||||
lt.alert.category_t.status_notification)
|
gt.alert.category_t.status_notification)
|
||||||
if self.config.debugAlerts:
|
if self.config.debugAlerts:
|
||||||
alertMask |= lt.alert.category_t.debug_notification
|
alertMask |= gt.alert.category_t.debug_notification
|
||||||
self.session.set_alert_mask(alertMask)
|
self.session.set_alert_mask(alertMask)
|
||||||
|
|
||||||
settings = self.session.get_settings()
|
settings = self.session.get_settings()
|
||||||
|
@ -730,12 +739,12 @@ class Pyrrent2http(object):
|
||||||
settings["tracker_backoff"] = 0
|
settings["tracker_backoff"] = 0
|
||||||
### Непонятно, как заставить использовать прокси только для подключения к трекеру?
|
### Непонятно, как заставить использовать прокси только для подключения к трекеру?
|
||||||
if self.config.proxy is not None:
|
if self.config.proxy is not None:
|
||||||
ps = lt.proxy_settings()
|
ps = gt.proxy_settings()
|
||||||
# peer_ps = lt.proxy_settings()
|
# peer_ps = lt.proxy_settings()
|
||||||
# peer_ps.type = lt.proxy_type.none
|
# peer_ps.type = lt.proxy_type.none
|
||||||
ps.hostname = self.config.proxy['host']
|
ps.hostname = self.config.proxy['host']
|
||||||
ps.port = self.config.proxy['port']
|
ps.port = self.config.proxy['port']
|
||||||
ps.type = lt.proxy_type.socks5
|
ps.type = gt.proxy_type.socks5
|
||||||
# self.session.set_peer_proxy(peer_ps)
|
# self.session.set_peer_proxy(peer_ps)
|
||||||
self.session.set_proxy(ps)
|
self.session.set_proxy(ps)
|
||||||
settings['force_proxy'] = False
|
settings['force_proxy'] = False
|
||||||
|
@ -753,7 +762,7 @@ class Pyrrent2http(object):
|
||||||
strerror = e.args
|
strerror = e.args
|
||||||
logging.error(strerror)
|
logging.error(strerror)
|
||||||
else:
|
else:
|
||||||
self.session.load_state(lt.bdecode(bytes__))
|
self.session.load_state(gt.bdecode(bytes__))
|
||||||
|
|
||||||
rand = SystemRandom(time.time())
|
rand = SystemRandom(time.time())
|
||||||
portLower = self.config.listenPort
|
portLower = self.config.listenPort
|
||||||
|
@ -799,10 +808,10 @@ class Pyrrent2http(object):
|
||||||
logging.info('Added DHT router: %s:%d' % (host, port))
|
logging.info('Added DHT router: %s:%d' % (host, port))
|
||||||
logging.info('Setting encryption settings')
|
logging.info('Setting encryption settings')
|
||||||
try:
|
try:
|
||||||
encryptionSettings = lt.pe_settings()
|
encryptionSettings = gt.pe_settings()
|
||||||
encryptionSettings.out_enc_policy = lt.enc_policy(self.config.encryption)
|
encryptionSettings.out_enc_policy = gt.enc_policy(self.config.encryption)
|
||||||
encryptionSettings.in_enc_policy = lt.enc_policy(self.config.encryption)
|
encryptionSettings.in_enc_policy = gt.enc_policy(self.config.encryption)
|
||||||
encryptionSettings.allowed_enc_level = lt.enc_level.both
|
encryptionSettings.allowed_enc_level = gt.enc_level.both
|
||||||
encryptionSettings.prefer_rc4 = True
|
encryptionSettings.prefer_rc4 = True
|
||||||
self.session.set_pe_settings(encryptionSettings)
|
self.session.set_pe_settings(encryptionSettings)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -871,7 +880,7 @@ class Pyrrent2http(object):
|
||||||
def consumeAlerts(self):
|
def consumeAlerts(self):
|
||||||
alerts = self.session.pop_alerts()
|
alerts = self.session.pop_alerts()
|
||||||
for alert in alerts:
|
for alert in alerts:
|
||||||
if type(alert) == lt.save_resume_data_alert:
|
if type(alert) == gt.save_resume_data_alert:
|
||||||
self.processSaveResumeDataAlert(alert)
|
self.processSaveResumeDataAlert(alert)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -907,7 +916,7 @@ class Pyrrent2http(object):
|
||||||
|
|
||||||
def processSaveResumeDataAlert(self, alert):
|
def processSaveResumeDataAlert(self, alert):
|
||||||
logging.info('Saving resume data to: %s' % (encode_msg(self.config.resumeFile),))
|
logging.info('Saving resume data to: %s' % (encode_msg(self.config.resumeFile),))
|
||||||
data = lt.bencode(alert.resume_data)
|
data = gt.bencode(alert.resume_data)
|
||||||
try:
|
try:
|
||||||
with open(self.config.resumeFile, 'wb') as f:
|
with open(self.config.resumeFile, 'wb') as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
@ -918,9 +927,9 @@ class Pyrrent2http(object):
|
||||||
def saveResumeData(self, async_=False):
|
def saveResumeData(self, async_=False):
|
||||||
if not self.torrentHandle.status().need_save_resume or self.config.resumeFile == '':
|
if not self.torrentHandle.status().need_save_resume or self.config.resumeFile == '':
|
||||||
return False
|
return False
|
||||||
self.torrentHandle.save_resume_data(lt.save_resume_flags_t.flush_disk_cache)
|
self.torrentHandle.save_resume_data(gt.save_resume_flags_t.flush_disk_cache)
|
||||||
if not async_:
|
if not async_:
|
||||||
alert = self.waitForAlert(lt.save_resume_data_alert, 5)
|
alert = self.waitForAlert(gt.save_resume_data_alert, 5)
|
||||||
if alert is None:
|
if alert is None:
|
||||||
return False
|
return False
|
||||||
self.processSaveResumeDataAlert(alert)
|
self.processSaveResumeDataAlert(alert)
|
||||||
|
@ -930,7 +939,7 @@ class Pyrrent2http(object):
|
||||||
if self.config.stateFile == '':
|
if self.config.stateFile == '':
|
||||||
return
|
return
|
||||||
entry = self.session.save_state()
|
entry = self.session.save_state()
|
||||||
data = lt.bencode(entry)
|
data = gt.bencode(entry)
|
||||||
logging.info('Saving session state to: %s' % (encode_msg(self.config.stateFile),))
|
logging.info('Saving session state to: %s' % (encode_msg(self.config.stateFile),))
|
||||||
try:
|
try:
|
||||||
logging.info('Saving session state to: %s' % (encode_msg(self.config.stateFile),))
|
logging.info('Saving session state to: %s' % (encode_msg(self.config.stateFile),))
|
||||||
|
@ -973,14 +982,14 @@ class Pyrrent2http(object):
|
||||||
state = self.torrentHandle.status().state
|
state = self.torrentHandle.status().state
|
||||||
if state != state.checking_files and not self.config.keepFiles:
|
if state != state.checking_files and not self.config.keepFiles:
|
||||||
if not self.config.keepComplete and not self.config.keepIncomplete:
|
if not self.config.keepComplete and not self.config.keepIncomplete:
|
||||||
flag = int(lt.options_t.delete_files)
|
flag = int(gt.options_t.delete_files)
|
||||||
else:
|
else:
|
||||||
files = self.filesToRemove()
|
files = self.filesToRemove()
|
||||||
logging.info('Removing the torrent')
|
logging.info('Removing the torrent')
|
||||||
self.session.remove_torrent(self.torrentHandle, flag)
|
self.session.remove_torrent(self.torrentHandle, flag)
|
||||||
if flag > 0 or len(files) > 0:
|
if flag > 0 or len(files) > 0:
|
||||||
logging.info('Waiting for files to be removed')
|
logging.info('Waiting for files to be removed')
|
||||||
self.waitForAlert(lt.torrent_deleted_alert, 15)
|
self.waitForAlert(gt.torrent_deleted_alert, 15)
|
||||||
self.removeFiles(files)
|
self.removeFiles(files)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
|
@ -992,7 +1001,7 @@ class Pyrrent2http(object):
|
||||||
self.TorrentFS.Shutdown()
|
self.TorrentFS.Shutdown()
|
||||||
if self.session != None:
|
if self.session != None:
|
||||||
self.session.pause()
|
self.session.pause()
|
||||||
self.waitForAlert(lt.torrent_paused_alert, 10)
|
self.waitForAlert(gt.torrent_paused_alert, 10)
|
||||||
if self.torrentHandle is not None:
|
if self.torrentHandle is not None:
|
||||||
self.saveResumeData(False)
|
self.saveResumeData(False)
|
||||||
self.saveSessionState()
|
self.saveSessionState()
|
|
@ -0,0 +1,11 @@
|
||||||
|
import xbmc
|
||||||
|
from typing import Union
|
||||||
|
import chardet
|
||||||
|
|
||||||
|
|
||||||
|
def info(msg : Union[str, bytes]):
|
||||||
|
xbmc.log(f'INFO: {isinstance(msg, bytes) and msg.decode(chardet.detect(msg)["encoding"]) or msg}')
|
||||||
|
|
||||||
|
|
||||||
|
def error(msg: Union[str, bytes]):
|
||||||
|
xbmc.log(f'ERROR: {isinstance(msg, bytes) and msg.decode(chardet.detect(msg)["encoding"]) or msg}')
|
|
@ -0,0 +1,178 @@
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import chardet
|
||||||
|
import sys
|
||||||
|
import xbmc
|
||||||
|
|
||||||
|
from .structs import MediaType
|
||||||
|
|
||||||
|
SUBTITLES_FORMATS = ['.aqt', '.gsub', '.jss', '.sub', '.ttxt', '.pjs', '.psb', '.rt', '.smi', '.stl',
|
||||||
|
'.ssf', '.srt', '.ssa', '.ass', '.usf', '.idx']
|
||||||
|
VIDEO_FORMATS = ['.mkv', '.mp4', '.avi', '.ogv', '.ts', '.flv', '.webm', '.vob', '.3gp', '.mpg', '.mpeg']
|
||||||
|
|
||||||
|
class Struct(dict):
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return self[attr]
|
||||||
|
|
||||||
|
def __setattr__(self, attr, value):
|
||||||
|
self[attr] = value
|
||||||
|
|
||||||
|
|
||||||
|
def uri2path(uri: str) -> str:
|
||||||
|
uri_path: str = ''
|
||||||
|
if uri[1] == ':' and sys.platform.startswith('win'):
|
||||||
|
uri = 'file:///' + uri
|
||||||
|
file_uri = urllib.parse.urlparse(uri)
|
||||||
|
if file_uri.scheme == 'file':
|
||||||
|
uri_path = file_uri.path
|
||||||
|
if uri_path != '' and sys.platform.startswith('win') and (os.path.sep == uri_path[0] or uri_path[0] == '/'):
|
||||||
|
uri_path = uri_path[1:]
|
||||||
|
abs_path = os.path.abspath(urllib.parse.unquote(uri_path))
|
||||||
|
return localize_path(abs_path)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_media_type(name):
|
||||||
|
ext = os.path.splitext(name)[1]
|
||||||
|
if ext in SUBTITLES_FORMATS:
|
||||||
|
return MediaType.SUBTITLES
|
||||||
|
else:
|
||||||
|
mime_type = mimetypes.guess_type(name)[0]
|
||||||
|
if mime_type is None:
|
||||||
|
if ext.lower() in VIDEO_FORMATS:
|
||||||
|
return MediaType.VIDEO
|
||||||
|
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 unicode_msg(tmpl, args):
|
||||||
|
msg = isinstance(tmpl, str) and tmpl or tmpl.decode(chardet.detect(tmpl)['encoding'])
|
||||||
|
arg_ = []
|
||||||
|
for a in args:
|
||||||
|
arg_.append(isinstance(a, str) and a or a.decode(chardet.detect(a)['encoding']))
|
||||||
|
return msg % tuple(arg_)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_msg(msg):
|
||||||
|
msg = isinstance(msg, str) and msg.encode(
|
||||||
|
sys.getfilesystemencoding() != 'ascii' and sys.getfilesystemencoding() or 'utf-8') or msg
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def localize_path(path):
|
||||||
|
if isinstance(path, bytes):
|
||||||
|
path = path.decode(chardet.detect(path)['encoding'])
|
||||||
|
# if not sys.platform.startswith('win'):
|
||||||
|
# path = path.encode(
|
||||||
|
# (sys.getfilesystemencoding() not in ('ascii', 'ANSI_X3.4-1968')) and sys.getfilesystemencoding() or 'utf-8')
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def can_bind(host, port):
|
||||||
|
"""
|
||||||
|
Checks we can bind to specified host and port
|
||||||
|
|
||||||
|
:param host: Host
|
||||||
|
:param port: Port
|
||||||
|
:return: True if bind succeed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.bind((host, port))
|
||||||
|
s.close()
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def find_free_port(host):
|
||||||
|
"""
|
||||||
|
Finds free TCP port that can be used for binding
|
||||||
|
|
||||||
|
:param host: Host
|
||||||
|
:return: Free port
|
||||||
|
"""
|
||||||
|
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 ensure_fs_encoding(string):
|
||||||
|
if isinstance(string, bytes):
|
||||||
|
string = string.decode('utf-8')
|
||||||
|
return string.encode(sys.getfilesystemencoding() or 'utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def get_platform():
|
||||||
|
ret = {
|
||||||
|
"arch": sys.maxsize > 2 ** 32 and "x64" or "x86",
|
||||||
|
}
|
||||||
|
if xbmc.getCondVisibility("system.platform.android"):
|
||||||
|
ret["os"] = "android"
|
||||||
|
if "arm" in os.uname()[4] or "aarch64" in os.uname()[4]:
|
||||||
|
ret["arch"] = "arm"
|
||||||
|
elif xbmc.getCondVisibility("system.platform.linux"):
|
||||||
|
ret["os"] = "linux"
|
||||||
|
uname = os.uname()[4]
|
||||||
|
if "arm" in uname:
|
||||||
|
if "armv7" in uname:
|
||||||
|
ret["arch"] = "armv7"
|
||||||
|
else:
|
||||||
|
ret["arch"] = "armv6"
|
||||||
|
elif "mips" in uname:
|
||||||
|
ret["arch"] = 'mipsel'
|
||||||
|
elif "aarch64" in uname:
|
||||||
|
if sys.maxsize > 2147483647: # is_64bit_system
|
||||||
|
ret["arch"] = 'aarch64'
|
||||||
|
else:
|
||||||
|
ret["arch"] = "armv7" # 32-bit userspace
|
||||||
|
elif xbmc.getCondVisibility("system.platform.windows"):
|
||||||
|
ret["os"] = "windows"
|
||||||
|
elif xbmc.getCondVisibility("system.platform.osx"):
|
||||||
|
ret["os"] = "darwin"
|
||||||
|
elif xbmc.getCondVisibility("system.platform.ios"):
|
||||||
|
ret["os"] = "ios"
|
||||||
|
ret["arch"] = "arm"
|
||||||
|
|
||||||
|
ret = get_system(ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_system(ret):
|
||||||
|
ret["system"] = ''
|
||||||
|
if ret["os"] == 'windows':
|
||||||
|
ret["system"] = 'windows_' + ret['arch']
|
||||||
|
elif ret["os"] == "linux" and ret["arch"] == "x64":
|
||||||
|
ret["system"] = 'linux_x86_64'
|
||||||
|
elif ret["os"] == "linux" and ret["arch"] == "x86":
|
||||||
|
ret["system"] = 'linux_x86'
|
||||||
|
elif ret["os"] == "linux" and "aarch64" in ret["arch"]:
|
||||||
|
ret["system"] = 'linux_' + ret["arch"]
|
||||||
|
elif ret["os"] == "linux" and ("arm" in ret["arch"] or 'mips' in ret["arch"]):
|
||||||
|
ret["system"] = 'linux_' + ret["arch"]
|
||||||
|
elif ret["os"] == "android":
|
||||||
|
if ret["arch"] == 'arm':
|
||||||
|
ret["system"] = 'android_armv7'
|
||||||
|
else:
|
||||||
|
ret["system"] = 'android_x86'
|
||||||
|
elif ret["os"] == "darwin":
|
||||||
|
ret["system"] = 'darwin'
|
||||||
|
elif ret["os"] == "ios" and ret["arch"] == "arm":
|
||||||
|
ret["system"] = 'ios_arm'
|
||||||
|
return ret
|
|
@ -1,402 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
import chardet
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import xbmc
|
|
||||||
|
|
||||||
from . import SessionStatus, FileStatus, PeerInfo
|
|
||||||
from . import pyrrent2http
|
|
||||||
from .error import Error
|
|
||||||
from .structs import Encryption
|
|
||||||
from .util import can_bind, find_free_port, localize_path, uri2path, detect_media_type
|
|
||||||
|
|
||||||
LOGGING = True
|
|
||||||
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""
|
|
||||||
This is python binding class to pyrrent2http client.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _log(self, message):
|
|
||||||
if self.logger:
|
|
||||||
self.logger(message)
|
|
||||||
else:
|
|
||||||
xbmc.log("[pyrrent2http] %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 binaries_path: Path to torrent2http binaries
|
|
||||||
: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
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _validate_save_path(path):
|
|
||||||
"""
|
|
||||||
Ensures download path can be accessed locally.
|
|
||||||
|
|
||||||
:param path: Download path
|
|
||||||
:return: Translated path
|
|
||||||
"""
|
|
||||||
import xbmc
|
|
||||||
path = xbmc.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=None):
|
|
||||||
"""
|
|
||||||
Starts pyrrent2http client with specified settings. If it can be started in startup_timeout seconds, exception
|
|
||||||
will be raised.
|
|
||||||
|
|
||||||
:param start_index: File index to start download instantly, if not specified, downloading will be paused, until
|
|
||||||
any file requested
|
|
||||||
"""
|
|
||||||
|
|
||||||
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 pyrrent2http", 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 = {
|
|
||||||
'torrentConnectBoost': self.torrent_connect_boost,
|
|
||||||
'trackers': ",".join(self.trackers),
|
|
||||||
'proxy': self.proxy,
|
|
||||||
'resumeFile': self.resume_file,
|
|
||||||
'minReconnectTime': self.min_reconnect_time,
|
|
||||||
'enableUPNP': self.enable_upnp,
|
|
||||||
'showAllStats': self.log_stats,
|
|
||||||
'debugAlerts': self.debug_alerts,
|
|
||||||
'keepComplete': self.keep_complete,
|
|
||||||
'dhtRouters': ",".join(self.dht_routers),
|
|
||||||
'userAgent': self.user_agent,
|
|
||||||
'enableLSD': self.enable_lsd,
|
|
||||||
'uri': self.uri,
|
|
||||||
'randomPort': self.use_random_port,
|
|
||||||
'noSparseFile': self.no_sparse,
|
|
||||||
'maxUploadRate': self.upload_kbps,
|
|
||||||
'downloadPath': download_path,
|
|
||||||
'showOverallProgress': self.log_overall_progress,
|
|
||||||
'enableDHT': self.enable_dht,
|
|
||||||
'showFilesProgress': self.log_files_progress,
|
|
||||||
'requestTimeout': self.request_timeout,
|
|
||||||
'bindAddress': "%s:%s" % (self.bind_host, self.bind_port),
|
|
||||||
'maxDownloadRate': self.download_kbps,
|
|
||||||
'connectionSpeed': self.connection_speed,
|
|
||||||
'keepIncomplete': self.keep_incomplete,
|
|
||||||
'enableTCP': self.enable_tcp,
|
|
||||||
'listenPort': self.listen_port,
|
|
||||||
'keepFiles': self.keep_files,
|
|
||||||
'stateFile': self.state_file,
|
|
||||||
'peerConnectTimeout': self.peer_connect_timeout,
|
|
||||||
'maxFailCount': self.max_failcount,
|
|
||||||
'showPiecesProgress': self.log_pieces_progress,
|
|
||||||
'idleTimeout': self.max_idle_timeout,
|
|
||||||
# 'fileIndex': start_index,
|
|
||||||
'connectionsLimit': self.connections_limit,
|
|
||||||
'enableScrape': self.enable_scrape,
|
|
||||||
'enableUTP': self.enable_utp,
|
|
||||||
'encryption': self.encryption,
|
|
||||||
'enableNATPMP': self.enable_natpmp
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
self._log("Invoking pyrrent2http")
|
|
||||||
|
|
||||||
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,))
|
|
||||||
|
|
||||||
pyrrent2http.logging = Logging(self._log)
|
|
||||||
|
|
||||||
self.pyrrent2http = pyrrent2http.Pyrrent2http(**kwargs)
|
|
||||||
self.pyrrent2http.startSession()
|
|
||||||
self.pyrrent2http.startServices()
|
|
||||||
self.pyrrent2http.addTorrent()
|
|
||||||
self.pyrrent2http.startHTTP()
|
|
||||||
self.pyrrent2http_loop = threading.Thread(target=self.pyrrent2http.loop)
|
|
||||||
self.pyrrent2http_loop.start()
|
|
||||||
|
|
||||||
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 pyrrent2http, 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 pyrrent2http, time is out", Error.TIMEOUT)
|
|
||||||
self._log("pyrrent2http successfully started.")
|
|
||||||
|
|
||||||
def activate_file(self, index):
|
|
||||||
self.pyrrent2http.TorrentFS.file(index)
|
|
||||||
|
|
||||||
def pause(self):
|
|
||||||
self.pyrrent2http.pause = True
|
|
||||||
|
|
||||||
def resume(self):
|
|
||||||
self.pyrrent2http.pause = False
|
|
||||||
|
|
||||||
def check_torrent_error(self, status=None):
|
|
||||||
"""
|
|
||||||
It is recommended to call this method periodically to check if any libtorrent errors occurred.
|
|
||||||
Usually libtorrent sets error if it can't download or parse torrent file by specified URI.
|
|
||||||
Note that pyrrent2http remains started after such error, so you need to shutdown it manually.
|
|
||||||
|
|
||||||
:param status: Pass return of status() method if you don't want status() called twice
|
|
||||||
"""
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Returns libtorrent session status. See SessionStatus named tuple.
|
|
||||||
|
|
||||||
:rtype : SessionStatus
|
|
||||||
:param timeout: pyrrent2http client request timeout
|
|
||||||
"""
|
|
||||||
status = self.pyrrent2http.Status()
|
|
||||||
status = SessionStatus(**status)
|
|
||||||
return status
|
|
||||||
|
|
||||||
def list(self, media_types=None, timeout=10):
|
|
||||||
"""
|
|
||||||
Returns list of files in the torrent (see FileStatus named tuple).
|
|
||||||
Note that it will return None if torrent file is not loaded yet by pyrrent2http client, so you may need to call
|
|
||||||
this method periodically until results are returned.
|
|
||||||
|
|
||||||
:param media_types: List of media types (see MediaType constants)
|
|
||||||
:param timeout: pyrrent2http client request timeout
|
|
||||||
:rtype : list of FileStatus
|
|
||||||
:return: List of files of specified media types or None if torrent is not loaded yet
|
|
||||||
"""
|
|
||||||
files = self.pyrrent2http.Ls()['files']
|
|
||||||
if files:
|
|
||||||
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 list_from_info(self, media_types=None):
|
|
||||||
try:
|
|
||||||
info = pyrrent2http.lt.torrent_info(uri2path(self.uri))
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
files = []
|
|
||||||
for i in range(info.num_files()):
|
|
||||||
f = info.file_at(i)
|
|
||||||
Url = 'http://' + "%s:%s" % (self.bind_host, self.bind_port) + '/files/' + urllib.parse.quote(f.path)
|
|
||||||
files.append({
|
|
||||||
'name': localize_path(f.path),
|
|
||||||
'size': f.size,
|
|
||||||
'offset': f.offset,
|
|
||||||
'media_type': media_types is not None and detect_media_type(f.path) or '',
|
|
||||||
'download': 0,
|
|
||||||
'progress': 0.0,
|
|
||||||
'save_path': '',
|
|
||||||
'url': Url
|
|
||||||
})
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Returns file in the torrent with specified index (see FileStatus named tuple)
|
|
||||||
Note that it will return None if torrent file is not loaded yet by pyrrent2http client, so you may need to call
|
|
||||||
this method periodically until results are returned.
|
|
||||||
|
|
||||||
:param file_index: Requested file's index
|
|
||||||
:param timeout: pyrrent2http client request timeout
|
|
||||||
:return: File with specified index
|
|
||||||
:rtype: FileStatus
|
|
||||||
"""
|
|
||||||
filestatus = self.pyrrent2http.Ls(file_index)
|
|
||||||
try:
|
|
||||||
return FileStatus(**filestatus)
|
|
||||||
except:
|
|
||||||
raise Error("Requested file index (%d) is invalid" % (file_index,), Error.INVALID_FILE_INDEX,
|
|
||||||
file_index=file_index)
|
|
||||||
|
|
||||||
def peers(self, timeout=10):
|
|
||||||
"""
|
|
||||||
Returns list of peers connected (see PeerInfo named tuple).
|
|
||||||
|
|
||||||
:param timeout: pyrrent2http client request timeout
|
|
||||||
:return: List of peers
|
|
||||||
:rtype: list of PeerInfo
|
|
||||||
"""
|
|
||||||
peers = self.pyrrent2http.Peers()['peers']
|
|
||||||
if peers:
|
|
||||||
return [PeerInfo(**p) for p in peers]
|
|
||||||
|
|
||||||
def is_alive(self):
|
|
||||||
return self.pyrrent2http_loop.is_alive()
|
|
||||||
|
|
||||||
def wait_on_close(self, wait_timeout=10):
|
|
||||||
"""
|
|
||||||
By default, close() method sends shutdown command to pyrrent2http, stops logging and returns immediately, not
|
|
||||||
waiting while pyrrent2http exits. It can be handy to wait pyrrent2http to view log messages during shutdown.
|
|
||||||
So call this method with reasonable timeout before calling close().
|
|
||||||
|
|
||||||
:param wait_timeout: Time in seconds to wait until pyrrent2http client shut down
|
|
||||||
"""
|
|
||||||
self.wait_on_close_timeout = wait_timeout
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""
|
|
||||||
Shuts down pyrrent2http and stops logging. If wait_on_close() was called earlier, it will wait until
|
|
||||||
pyrrent2http successfully exits.
|
|
||||||
"""
|
|
||||||
if self.is_alive():
|
|
||||||
self._log("Shutting down pyrrent2http...")
|
|
||||||
self.pyrrent2http.shutdown()
|
|
||||||
finished = False
|
|
||||||
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 pyrrent2http thread")
|
|
||||||
else:
|
|
||||||
self._log("pyrrent2http successfully shut down.")
|
|
||||||
self.wait_on_close_timeout = None
|
|
||||||
self._log("pyrrent2http successfully shut down.")
|
|
||||||
self.started = False
|
|
||||||
self.logpipe = None
|
|
||||||
self.process = None
|
|
|
@ -1,116 +0,0 @@
|
||||||
import mimetypes
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
import chardet
|
|
||||||
import sys
|
|
||||||
import xbmc
|
|
||||||
|
|
||||||
from .structs import MediaType
|
|
||||||
|
|
||||||
SUBTITLES_FORMATS = ['.aqt', '.gsub', '.jss', '.sub', '.ttxt', '.pjs', '.psb', '.rt', '.smi', '.stl',
|
|
||||||
'.ssf', '.srt', '.ssa', '.ass', '.usf', '.idx']
|
|
||||||
|
|
||||||
|
|
||||||
class Struct(dict):
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return self[attr]
|
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
|
||||||
self[attr] = value
|
|
||||||
|
|
||||||
|
|
||||||
def uri2path(uri):
|
|
||||||
if uri[1] == ':' and sys.platform.startswith('win'):
|
|
||||||
uri = 'file:///' + uri
|
|
||||||
fileUri = urllib.parse.urlparse(uri)
|
|
||||||
if fileUri.scheme == 'file':
|
|
||||||
uriPath = fileUri.path
|
|
||||||
if uriPath != '' and sys.platform.startswith('win') and (os.path.sep == uriPath[0] or uriPath[0] == '/'):
|
|
||||||
uriPath = uriPath[1:]
|
|
||||||
absPath = os.path.abspath(urllib.parse.unquote(uriPath))
|
|
||||||
return localize_path(absPath)
|
|
||||||
|
|
||||||
|
|
||||||
def detect_media_type(name):
|
|
||||||
ext = os.path.splitext(name)[1]
|
|
||||||
if ext in 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 unicode_msg(tmpl, args):
|
|
||||||
msg = isinstance(tmpl, str) and tmpl or tmpl.decode(chardet.detect(tmpl)['encoding'])
|
|
||||||
arg_ = []
|
|
||||||
for a in args:
|
|
||||||
arg_.append(isinstance(a, str) and a or a.decode(chardet.detect(a)['encoding']))
|
|
||||||
return msg % tuple(arg_)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_msg(msg):
|
|
||||||
msg = isinstance(msg, str) and msg.encode(
|
|
||||||
sys.getfilesystemencoding() != 'ascii' and sys.getfilesystemencoding() or 'utf-8') or msg
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def localize_path(path):
|
|
||||||
if isinstance(path, bytes):
|
|
||||||
path = path.decode(chardet.detect(path)['encoding'])
|
|
||||||
# if not sys.platform.startswith('win'):
|
|
||||||
# path = path.encode(
|
|
||||||
# (sys.getfilesystemencoding() not in ('ascii', 'ANSI_X3.4-1968')) and sys.getfilesystemencoding() or 'utf-8')
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def can_bind(host, port):
|
|
||||||
"""
|
|
||||||
Checks we can bind to specified host and port
|
|
||||||
|
|
||||||
:param host: Host
|
|
||||||
:param port: Port
|
|
||||||
:return: True if bind succeed
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.bind((host, port))
|
|
||||||
s.close()
|
|
||||||
except socket.error:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def find_free_port(host):
|
|
||||||
"""
|
|
||||||
Finds free TCP port that can be used for binding
|
|
||||||
|
|
||||||
:param host: Host
|
|
||||||
:return: Free port
|
|
||||||
"""
|
|
||||||
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 ensure_fs_encoding(string):
|
|
||||||
if isinstance(string, bytes):
|
|
||||||
string = string.decode('utf-8')
|
|
||||||
return string.encode(sys.getfilesystemencoding() or 'utf-8')
|
|
Loading…
Reference in New Issue