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"?>
|
||||
<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>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
<import addon="script.module.libtorrent" version="1.2.0"/>
|
||||
<import addon="script.module.chardet" />
|
||||
</requires>
|
||||
<extension point="xbmc.python.module" library="lib"/>
|
||||
|
@ -17,5 +16,5 @@
|
|||
<description lang="ru">Обеспечивает последовательную (sequential) загрузку торрентов для потокового онлайн просмотра через HTTP. Основан на библиотеке LibTorrent.</description>
|
||||
<description lang="en">Provides sequential torrent downloading for online streaming video and other media over HTTP.</description>
|
||||
<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>
|
||||
|
|
|
@ -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 -*-
|
||||
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 io
|
||||
import os
|
||||
import socketserver
|
||||
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
|
||||
|
||||
|
||||
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'):
|
||||
from ctypes import *
|
||||
|
||||
|
@ -72,9 +80,9 @@ if not hasattr(os, 'getppid'):
|
|||
|
||||
|
||||
def getppid():
|
||||
'''
|
||||
"""
|
||||
:return: The pid of the parent of this process.
|
||||
'''
|
||||
"""
|
||||
pe = PROCESSENTRY32()
|
||||
pe.dwSize = ctypes.sizeof(PROCESSENTRY32)
|
||||
mypid = GetCurrentProcessId()
|
||||
|
@ -162,7 +170,8 @@ class TorrentFile(object):
|
|||
self.tfs = tfs
|
||||
self.fileEntry = fileEntry
|
||||
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.save_path = savePath
|
||||
self.index = index
|
||||
|
@ -183,7 +192,7 @@ class TorrentFile(object):
|
|||
return None
|
||||
if self.filePtr is None:
|
||||
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()
|
||||
time.sleep(0.5)
|
||||
if os.getenv('ANDROID_ROOT'):
|
||||
|
@ -194,7 +203,7 @@ class TorrentFile(object):
|
|||
|
||||
def log(self, message):
|
||||
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):
|
||||
startPiece, _ = self.pieceFromOffset(1)
|
||||
|
@ -545,10 +554,10 @@ def HttpHandlerFactory():
|
|||
return HttpHandler
|
||||
|
||||
|
||||
class Pyrrent2http(object):
|
||||
class Gorrent2http(object):
|
||||
pause = False
|
||||
|
||||
def __init__(self, uri='', bindAddress='localhost:5001', downloadPath='.',
|
||||
def __init__(self, uri='', bind_address='localhost:5001', download_path='.',
|
||||
idleTimeout=-1, keepComplete=False,
|
||||
keepIncomplete=False, keepFiles=False, showAllStats=False,
|
||||
showOverallProgress=False, showFilesProgress=False,
|
||||
|
@ -568,8 +577,8 @@ class Pyrrent2http(object):
|
|||
|
||||
self.config = Struct()
|
||||
self.config.uri = uri
|
||||
self.config.bindAddress = bindAddress
|
||||
self.config.downloadPath = downloadPath
|
||||
self.config.bindAddress = bind_address
|
||||
self.config.downloadPath = download_path
|
||||
self.config.idleTimeout = idleTimeout
|
||||
self.config.keepComplete = keepComplete
|
||||
self.config.keepIncomplete = keepIncomplete
|
||||
|
@ -618,7 +627,7 @@ class Pyrrent2http(object):
|
|||
try:
|
||||
absPath = uri2path(uri)
|
||||
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:
|
||||
strerror = e.args
|
||||
logging.error('Build torrent params error is (%s)' % (strerror,))
|
||||
|
@ -639,7 +648,7 @@ class Pyrrent2http(object):
|
|||
logging.error(strerror)
|
||||
if self.config.noSparseFile or self.magnet:
|
||||
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
|
||||
|
||||
def addTorrent(self):
|
||||
|
@ -704,14 +713,14 @@ class Pyrrent2http(object):
|
|||
|
||||
def startSession(self):
|
||||
logging.info('Starting session...')
|
||||
self.session = lt.session(lt.fingerprint('LT', lt.version_major, lt.version_minor, 0, 0),
|
||||
flags=int(lt.session_flags_t.add_default_plugins))
|
||||
alertMask = (lt.alert.category_t.error_notification |
|
||||
lt.alert.category_t.storage_notification |
|
||||
lt.alert.category_t.tracker_notification |
|
||||
lt.alert.category_t.status_notification)
|
||||
self.session = gt.session(gt.fingerprint('LT', gt.version_major, gt.version_minor, 0, 0),
|
||||
flags=int(gt.session_flags_t.add_default_plugins))
|
||||
alertMask = (gt.alert.category_t.error_notification |
|
||||
gt.alert.category_t.storage_notification |
|
||||
gt.alert.category_t.tracker_notification |
|
||||
gt.alert.category_t.status_notification)
|
||||
if self.config.debugAlerts:
|
||||
alertMask |= lt.alert.category_t.debug_notification
|
||||
alertMask |= gt.alert.category_t.debug_notification
|
||||
self.session.set_alert_mask(alertMask)
|
||||
|
||||
settings = self.session.get_settings()
|
||||
|
@ -730,12 +739,12 @@ class Pyrrent2http(object):
|
|||
settings["tracker_backoff"] = 0
|
||||
### Непонятно, как заставить использовать прокси только для подключения к трекеру?
|
||||
if self.config.proxy is not None:
|
||||
ps = lt.proxy_settings()
|
||||
ps = gt.proxy_settings()
|
||||
# peer_ps = lt.proxy_settings()
|
||||
# peer_ps.type = lt.proxy_type.none
|
||||
ps.hostname = self.config.proxy['host']
|
||||
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_proxy(ps)
|
||||
settings['force_proxy'] = False
|
||||
|
@ -753,7 +762,7 @@ class Pyrrent2http(object):
|
|||
strerror = e.args
|
||||
logging.error(strerror)
|
||||
else:
|
||||
self.session.load_state(lt.bdecode(bytes__))
|
||||
self.session.load_state(gt.bdecode(bytes__))
|
||||
|
||||
rand = SystemRandom(time.time())
|
||||
portLower = self.config.listenPort
|
||||
|
@ -799,10 +808,10 @@ class Pyrrent2http(object):
|
|||
logging.info('Added DHT router: %s:%d' % (host, port))
|
||||
logging.info('Setting encryption settings')
|
||||
try:
|
||||
encryptionSettings = lt.pe_settings()
|
||||
encryptionSettings.out_enc_policy = lt.enc_policy(self.config.encryption)
|
||||
encryptionSettings.in_enc_policy = lt.enc_policy(self.config.encryption)
|
||||
encryptionSettings.allowed_enc_level = lt.enc_level.both
|
||||
encryptionSettings = gt.pe_settings()
|
||||
encryptionSettings.out_enc_policy = gt.enc_policy(self.config.encryption)
|
||||
encryptionSettings.in_enc_policy = gt.enc_policy(self.config.encryption)
|
||||
encryptionSettings.allowed_enc_level = gt.enc_level.both
|
||||
encryptionSettings.prefer_rc4 = True
|
||||
self.session.set_pe_settings(encryptionSettings)
|
||||
except Exception as e:
|
||||
|
@ -871,7 +880,7 @@ class Pyrrent2http(object):
|
|||
def consumeAlerts(self):
|
||||
alerts = self.session.pop_alerts()
|
||||
for alert in alerts:
|
||||
if type(alert) == lt.save_resume_data_alert:
|
||||
if type(alert) == gt.save_resume_data_alert:
|
||||
self.processSaveResumeDataAlert(alert)
|
||||
break
|
||||
|
||||
|
@ -907,7 +916,7 @@ class Pyrrent2http(object):
|
|||
|
||||
def processSaveResumeDataAlert(self, alert):
|
||||
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:
|
||||
with open(self.config.resumeFile, 'wb') as f:
|
||||
f.write(data)
|
||||
|
@ -918,9 +927,9 @@ class Pyrrent2http(object):
|
|||
def saveResumeData(self, async_=False):
|
||||
if not self.torrentHandle.status().need_save_resume or self.config.resumeFile == '':
|
||||
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_:
|
||||
alert = self.waitForAlert(lt.save_resume_data_alert, 5)
|
||||
alert = self.waitForAlert(gt.save_resume_data_alert, 5)
|
||||
if alert is None:
|
||||
return False
|
||||
self.processSaveResumeDataAlert(alert)
|
||||
|
@ -930,7 +939,7 @@ class Pyrrent2http(object):
|
|||
if self.config.stateFile == '':
|
||||
return
|
||||
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),))
|
||||
try:
|
||||
logging.info('Saving session state to: %s' % (encode_msg(self.config.stateFile),))
|
||||
|
@ -973,14 +982,14 @@ class Pyrrent2http(object):
|
|||
state = self.torrentHandle.status().state
|
||||
if state != state.checking_files and not self.config.keepFiles:
|
||||
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:
|
||||
files = self.filesToRemove()
|
||||
logging.info('Removing the torrent')
|
||||
self.session.remove_torrent(self.torrentHandle, flag)
|
||||
if flag > 0 or len(files) > 0:
|
||||
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)
|
||||
|
||||
def shutdown(self):
|
||||
|
@ -992,7 +1001,7 @@ class Pyrrent2http(object):
|
|||
self.TorrentFS.Shutdown()
|
||||
if self.session != None:
|
||||
self.session.pause()
|
||||
self.waitForAlert(lt.torrent_paused_alert, 10)
|
||||
self.waitForAlert(gt.torrent_paused_alert, 10)
|
||||
if self.torrentHandle is not None:
|
||||
self.saveResumeData(False)
|
||||
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