рабочая версия плагина
parent
5a640c4ae4
commit
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.0.0" 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>
|
||||||
|
|
|
@ -5,30 +5,45 @@ import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
import chardet
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import xbmc
|
import xbmc
|
||||||
|
import xbmcvfs
|
||||||
|
from . import log as logging
|
||||||
|
|
||||||
from . import SessionStatus, FileStatus, PeerInfo
|
from . import FileStatus, SessionStatus
|
||||||
from . import pyrrent2http
|
|
||||||
from .error import Error
|
from .error import Error
|
||||||
from .structs import Encryption
|
from .structs import Encryption
|
||||||
from .util import can_bind, find_free_port, localize_path, uri2path, detect_media_type
|
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
|
LOGGING = True
|
||||||
|
|
||||||
|
|
||||||
class Engine:
|
class Engine:
|
||||||
"""
|
"""
|
||||||
This is python binding class to pyrrent2http client.
|
This is python binding class to gorrent2http client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _log(self, message):
|
def _log(self, message):
|
||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger(message)
|
self.logger(message)
|
||||||
else:
|
else:
|
||||||
xbmc.log("[pyrrent2http] %s" % message)
|
xbmc.log("[gorrent2http] %s" % message)
|
||||||
|
|
||||||
def __init__(self, uri=None, platform=None, download_path=".",
|
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,
|
bind_host='127.0.0.1', bind_port=5001, connections_limit=200, download_kbps=-1, upload_kbps=-1,
|
||||||
|
@ -45,7 +60,6 @@ class Engine:
|
||||||
start() method.
|
start() method.
|
||||||
|
|
||||||
:param uri: Torrent URI (magnet://, file:// or http://)
|
: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 platform: Object with two methods implemented: arch() and system()
|
||||||
:param download_path: Torrent download path
|
:param download_path: Torrent download path
|
||||||
:param bind_host: Bind host of torrent2http
|
:param bind_host: Bind host of torrent2http
|
||||||
|
@ -134,16 +148,12 @@ class Engine:
|
||||||
self.started = False
|
self.started = False
|
||||||
self.proxy = proxy
|
self.proxy = proxy
|
||||||
|
|
||||||
|
self.message_logger = None
|
||||||
|
self.run_message_logger = True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _validate_save_path(path):
|
def _validate_save_path(path):
|
||||||
"""
|
path = xbmcvfs.translatePath(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 "://" in path:
|
||||||
if sys.platform.startswith('win') and path.lower().startswith("smb://"):
|
if sys.platform.startswith('win') and path.lower().startswith("smb://"):
|
||||||
path = path.replace("smb:", "").replace("/", "\\")
|
path = path.replace("smb:", "").replace("/", "\\")
|
||||||
|
@ -153,67 +163,16 @@ class Engine:
|
||||||
raise Error("Download path doesn't exist (%s)" % path, Error.INVALID_DOWNLOAD_PATH)
|
raise Error("Download path doesn't exist (%s)" % path, Error.INVALID_DOWNLOAD_PATH)
|
||||||
return localize_path(path)
|
return localize_path(path)
|
||||||
|
|
||||||
def start(self, start_index=None):
|
def start(self, start_index):
|
||||||
"""
|
|
||||||
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)
|
download_path = self._validate_save_path(self.download_path)
|
||||||
if not can_bind(self.bind_host, self.bind_port):
|
if not can_bind(self.bind_host, self.bind_port):
|
||||||
port = find_free_port(self.bind_host)
|
port = find_free_port(self.bind_host)
|
||||||
if port is False:
|
if port is False:
|
||||||
raise Error("Can't find port to bind pyrrent2http", Error.BIND_ERROR)
|
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._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.bind_port = port
|
||||||
|
|
||||||
kwargs = {
|
self._log("Invoking gorrent2http")
|
||||||
'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):
|
class Logging(object):
|
||||||
def __init__(self, _log):
|
def __init__(self, _log):
|
||||||
|
@ -227,15 +186,34 @@ class Engine:
|
||||||
if LOGGING:
|
if LOGGING:
|
||||||
self._log('ERROR: %s' % (message,))
|
self._log('ERROR: %s' % (message,))
|
||||||
|
|
||||||
pyrrent2http.logging = Logging(self._log)
|
gt.logging = Logging(self._log)
|
||||||
|
|
||||||
self.pyrrent2http = pyrrent2http.Pyrrent2http(**kwargs)
|
settings = gt.NewSettings()
|
||||||
self.pyrrent2http.startSession()
|
settings.DownloadPath = download_path
|
||||||
self.pyrrent2http.startServices()
|
settings.HttpBindHost = self.bind_host
|
||||||
self.pyrrent2http.addTorrent()
|
settings.HttpBindPort = self.bind_port
|
||||||
self.pyrrent2http.startHTTP()
|
settings.ListenPort = self.listen_port
|
||||||
self.pyrrent2http_loop = threading.Thread(target=self.pyrrent2http.loop)
|
settings.TorrentPath = uri2path(self.uri)
|
||||||
self.pyrrent2http_loop.start()
|
settings.MaxConnections = self.connections_limit
|
||||||
|
settings.Debug = self.debug_alerts
|
||||||
|
settings.KeepFiles = self.keep_files
|
||||||
|
settings.Proxy = len(self.proxy) > 0 and f'socks5://{self.proxy["host"]}:{self.proxy["port"]}' or ''
|
||||||
|
|
||||||
|
self.engine = gt.NewEngine(settings)
|
||||||
|
def msg_logger():
|
||||||
|
while self.run_message_logger:
|
||||||
|
msg = self.engine.GetMsg()
|
||||||
|
if msg not in ('__NO_MSG__', '__CLOSED__'):
|
||||||
|
xbmc.log(f'-= GORRENT =-: {msg}')
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.message_logger = threading.Thread(target=msg_logger)
|
||||||
|
self.message_logger.start()
|
||||||
|
|
||||||
|
self._log('starting torrent')
|
||||||
|
|
||||||
|
self.engine.StartTorrent(start_index)
|
||||||
|
|
||||||
|
self._log('waiting alive status set')
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
self.started = True
|
self.started = True
|
||||||
|
@ -243,7 +221,7 @@ class Engine:
|
||||||
while (time.time() - start) < self.startup_timeout:
|
while (time.time() - start) < self.startup_timeout:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
if not self.is_alive():
|
if not self.is_alive():
|
||||||
raise Error("Can't start pyrrent2http, see log for details", Error.PROCESS_ERROR)
|
raise Error("Can't start gorrent2http, see log for details", Error.PROCESS_ERROR)
|
||||||
try:
|
try:
|
||||||
# self.status(1)
|
# self.status(1)
|
||||||
initialized = True
|
initialized = True
|
||||||
|
@ -253,79 +231,77 @@ class Engine:
|
||||||
|
|
||||||
if not initialized:
|
if not initialized:
|
||||||
self.started = False
|
self.started = False
|
||||||
raise Error("Can't start pyrrent2http, time is out", Error.TIMEOUT)
|
raise Error("Can't start gorrent2http, time is out", Error.TIMEOUT)
|
||||||
self._log("pyrrent2http successfully started.")
|
self._log("gorrent2http successfully started.")
|
||||||
|
|
||||||
def activate_file(self, index):
|
|
||||||
self.pyrrent2http.TorrentFS.file(index)
|
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
self.pyrrent2http.pause = True
|
pass
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
self.pyrrent2http.pause = False
|
pass
|
||||||
|
|
||||||
def check_torrent_error(self, status=None):
|
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:
|
if not status:
|
||||||
status = self.status()
|
status = self.status()
|
||||||
if status.error:
|
if status.error:
|
||||||
raise Error("Torrent error: %s" % status.error, Error.TORRENT_ERROR, reason=status.error)
|
raise Error("Torrent error: %s" % status.error, Error.TORRENT_ERROR, reason=status.error)
|
||||||
|
|
||||||
def status(self, timeout=10):
|
def status(self, timeout=10):
|
||||||
"""
|
stat = self.engine.Status()
|
||||||
Returns libtorrent session status. See SessionStatus named tuple.
|
stat_kw = {
|
||||||
|
'download_rate': stat.DownloadRate,
|
||||||
:rtype : SessionStatus
|
'upload_rate': stat.UploadRate,
|
||||||
:param timeout: pyrrent2http client request timeout
|
'num_seeds': stat.Seeds,
|
||||||
"""
|
'name': '',
|
||||||
status = self.pyrrent2http.Status()
|
'state': 0,
|
||||||
status = SessionStatus(**status)
|
'state_str': '',
|
||||||
return status
|
'error': '',
|
||||||
|
'progress': 0,
|
||||||
def list(self, media_types=None, timeout=10):
|
'total_download': 0,
|
||||||
"""
|
'total_upload': 0,
|
||||||
Returns list of files in the torrent (see FileStatus named tuple).
|
'num_peers': 0,
|
||||||
Note that it will return None if torrent file is not loaded yet by pyrrent2http client, so you may need to call
|
'total_seeds': 0,
|
||||||
this method periodically until results are returned.
|
'total_peers': 0
|
||||||
|
}
|
||||||
:param media_types: List of media types (see MediaType constants)
|
return SessionStatus(**stat_kw)
|
||||||
: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):
|
def list_from_info(self, media_types=None):
|
||||||
try:
|
try:
|
||||||
info = pyrrent2http.lt.torrent_info(uri2path(self.uri))
|
info = gt.GetMetaFromFile(uri2path(self.uri))
|
||||||
except:
|
except:
|
||||||
|
import traceback
|
||||||
|
xbmc.log(f'info load exception: {traceback.format_exc()}')
|
||||||
return []
|
return []
|
||||||
files = []
|
files = []
|
||||||
for i in range(info.num_files()):
|
if len(info.Files) > 0:
|
||||||
f = info.file_at(i)
|
for i in range(len(info.Files)):
|
||||||
Url = 'http://' + "%s:%s" % (self.bind_host, self.bind_port) + '/files/' + urllib.parse.quote(f.path)
|
f = info.Files[i]
|
||||||
|
|
||||||
|
uri = 'http://' + "%s:%s" % (self.bind_host, self.bind_port) + '/files/' + urllib.parse.quote(
|
||||||
|
'/'.join(f.Path))
|
||||||
files.append({
|
files.append({
|
||||||
'name': localize_path(f.path),
|
'name': localize_path('/'.join(f.Path)),
|
||||||
'size': f.size,
|
'size': f.Length,
|
||||||
'offset': f.offset,
|
'offset': i,
|
||||||
'media_type': media_types is not None and detect_media_type(f.path) or '',
|
'media_type': media_types is not None and detect_media_type(f.Path[-1]) or '',
|
||||||
'download': 0,
|
'download': 0,
|
||||||
'progress': 0.0,
|
'progress': 0.0,
|
||||||
'save_path': '',
|
'save_path': '',
|
||||||
'url': Url
|
'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:
|
if len(files) > 0:
|
||||||
res = [FileStatus(index=index, **f) for index, f in enumerate(files)]
|
res = [FileStatus(index=index, **f) for index, f in enumerate(files)]
|
||||||
if media_types is not None:
|
if media_types is not None:
|
||||||
|
@ -333,57 +309,34 @@ class Engine:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def file_status(self, file_index, timeout=10):
|
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:
|
try:
|
||||||
return FileStatus(**filestatus)
|
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:
|
except:
|
||||||
raise Error("Requested file index (%d) is invalid" % (file_index,), Error.INVALID_FILE_INDEX,
|
raise Error("Requested file index (%d) is invalid" % (file_index,), Error.INVALID_FILE_INDEX,
|
||||||
file_index=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):
|
def is_alive(self):
|
||||||
return self.pyrrent2http_loop.is_alive()
|
return self.engine.IsAlive()
|
||||||
|
|
||||||
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):
|
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():
|
if self.is_alive():
|
||||||
self._log("Shutting down pyrrent2http...")
|
self._log("Shutting down gorrent2http...")
|
||||||
self.pyrrent2http.shutdown()
|
self.engine.Stop()
|
||||||
finished = False
|
finished = False
|
||||||
|
|
||||||
|
self.wait_on_close_timeout = 10
|
||||||
|
|
||||||
if self.wait_on_close_timeout is not None:
|
if self.wait_on_close_timeout is not None:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
while (time.time() - start) < self.wait_on_close_timeout:
|
while (time.time() - start) < self.wait_on_close_timeout:
|
||||||
|
@ -392,11 +345,12 @@ class Engine:
|
||||||
finished = True
|
finished = True
|
||||||
break
|
break
|
||||||
if not finished:
|
if not finished:
|
||||||
self._log("PANIC: Timeout occurred while shutting down pyrrent2http thread")
|
self._log("PANIC: Timeout occurred while shutting down gorrent2http thread")
|
||||||
else:
|
else:
|
||||||
self._log("pyrrent2http successfully shut down.")
|
self._log("gorrent2http successfully shut down.")
|
||||||
self.wait_on_close_timeout = None
|
self.wait_on_close_timeout = None
|
||||||
self._log("pyrrent2http successfully shut down.")
|
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
|
self.started = False
|
||||||
self.logpipe = None
|
|
||||||
self.process = None
|
|
|
@ -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}')
|
|
@ -24,16 +24,17 @@ class Struct(dict):
|
||||||
self[attr] = value
|
self[attr] = value
|
||||||
|
|
||||||
|
|
||||||
def uri2path(uri):
|
def uri2path(uri: str) -> str:
|
||||||
|
uri_path: str = ''
|
||||||
if uri[1] == ':' and sys.platform.startswith('win'):
|
if uri[1] == ':' and sys.platform.startswith('win'):
|
||||||
uri = 'file:///' + uri
|
uri = 'file:///' + uri
|
||||||
fileUri = urllib.parse.urlparse(uri)
|
file_uri = urllib.parse.urlparse(uri)
|
||||||
if fileUri.scheme == 'file':
|
if file_uri.scheme == 'file':
|
||||||
uriPath = fileUri.path
|
uri_path = file_uri.path
|
||||||
if uriPath != '' and sys.platform.startswith('win') and (os.path.sep == uriPath[0] or uriPath[0] == '/'):
|
if uri_path != '' and sys.platform.startswith('win') and (os.path.sep == uri_path[0] or uri_path[0] == '/'):
|
||||||
uriPath = uriPath[1:]
|
uri_path = uri_path[1:]
|
||||||
absPath = os.path.abspath(urllib.parse.unquote(uriPath))
|
abs_path = os.path.abspath(urllib.parse.unquote(uri_path))
|
||||||
return localize_path(absPath)
|
return localize_path(abs_path)
|
||||||
|
|
||||||
|
|
||||||
def detect_media_type(name):
|
def detect_media_type(name):
|
||||||
|
@ -114,3 +115,62 @@ def ensure_fs_encoding(string):
|
||||||
if isinstance(string, bytes):
|
if isinstance(string, bytes):
|
||||||
string = string.decode('utf-8')
|
string = string.decode('utf-8')
|
||||||
return string.encode(sys.getfilesystemencoding() or '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
|
Loading…
Reference in New Issue