321 lines
10 KiB
Python
321 lines
10 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# MIT License (see LICENSE.txt or https://opensource.org/licenses/MIT)
|
||
|
"""Implements various Helper functions"""
|
||
|
|
||
|
from __future__ import absolute_import, division, unicode_literals
|
||
|
import os
|
||
|
from time import time
|
||
|
from socket import timeout
|
||
|
from ssl import SSLError
|
||
|
|
||
|
try: # Python 3
|
||
|
from urllib.error import HTTPError, URLError
|
||
|
from urllib.request import Request, urlopen
|
||
|
except ImportError: # Python 2
|
||
|
from urllib2 import HTTPError, Request, URLError, urlopen
|
||
|
|
||
|
from . import config
|
||
|
from .kodiutils import (bg_progress_dialog, copy, delete, exists, get_setting, localize, log, mkdirs,
|
||
|
progress_dialog, set_setting, stat_file, translate_path, yesno_dialog)
|
||
|
from .unicodes import compat_path, from_unicode, to_unicode
|
||
|
|
||
|
|
||
|
def temp_path():
|
||
|
"""Return temporary path, usually ~/.kodi/userdata/addon_data/script.module.inputstreamhelper/temp/"""
|
||
|
tmp_path = translate_path(os.path.join(get_setting('temp_path', 'special://masterprofile/addon_data/script.module.inputstreamhelper'), 'temp', ''))
|
||
|
if not exists(tmp_path):
|
||
|
mkdirs(tmp_path)
|
||
|
|
||
|
return tmp_path
|
||
|
|
||
|
|
||
|
def update_temp_path(new_temp_path):
|
||
|
""""Updates temp_path and merges files."""
|
||
|
old_temp_path = temp_path()
|
||
|
|
||
|
set_setting('temp_path', new_temp_path)
|
||
|
if old_temp_path != temp_path():
|
||
|
from shutil import move
|
||
|
move(old_temp_path, temp_path())
|
||
|
|
||
|
|
||
|
def _http_request(url, headers=None, time_out=10):
|
||
|
"""Perform an HTTP request and return request"""
|
||
|
log(0, 'Request URL: {url}', url=url)
|
||
|
|
||
|
try:
|
||
|
if headers:
|
||
|
request = Request(url, headers=headers)
|
||
|
else:
|
||
|
request = Request(url)
|
||
|
req = urlopen(request, timeout=time_out)
|
||
|
log(0, 'Response code: {code}', code=req.getcode())
|
||
|
if 400 <= req.getcode() < 600:
|
||
|
raise HTTPError('HTTP %s Error for url: %s' % (req.getcode(), url), response=req)
|
||
|
except (HTTPError, URLError) as err:
|
||
|
log(2, 'Download failed with error {}'.format(err))
|
||
|
if yesno_dialog(localize(30004), '{line1}\n{line2}'.format(line1=localize(30063), line2=localize(30065))): # Internet down, try again?
|
||
|
return _http_request(url, headers, time_out)
|
||
|
return None
|
||
|
|
||
|
return req
|
||
|
|
||
|
|
||
|
def http_get(url):
|
||
|
"""Perform an HTTP GET request and return content"""
|
||
|
req = _http_request(url)
|
||
|
if req is None:
|
||
|
return None
|
||
|
|
||
|
content = req.read()
|
||
|
# NOTE: Do not log reponse (as could be large)
|
||
|
# log(0, 'Response: {response}', response=content)
|
||
|
return content.decode()
|
||
|
|
||
|
|
||
|
def http_download(url, message=None, checksum=None, hash_alg='sha1', dl_size=None, background=False): # pylint: disable=too-many-statements
|
||
|
"""Makes HTTP request and displays a progress dialog on download."""
|
||
|
if checksum:
|
||
|
from hashlib import sha1, md5
|
||
|
if hash_alg == 'sha1':
|
||
|
calc_checksum = sha1()
|
||
|
elif hash_alg == 'md5':
|
||
|
calc_checksum = md5()
|
||
|
else:
|
||
|
log(4, 'Invalid hash algorithm specified: {}'.format(hash_alg))
|
||
|
checksum = None
|
||
|
|
||
|
req = _http_request(url)
|
||
|
if req is None:
|
||
|
return None
|
||
|
|
||
|
filename = url.split('/')[-1]
|
||
|
if not message: # display "downloading [filename]"
|
||
|
message = localize(30015, filename=filename) # Downloading file
|
||
|
|
||
|
download_path = os.path.join(temp_path(), filename)
|
||
|
total_length = int(req.info().get('content-length'))
|
||
|
if dl_size and dl_size != total_length:
|
||
|
log(2, 'The given file size does not match the request!')
|
||
|
dl_size = total_length # Otherwise size check at end would fail even if dl succeeded
|
||
|
|
||
|
if background:
|
||
|
progress = bg_progress_dialog()
|
||
|
else:
|
||
|
progress = progress_dialog()
|
||
|
progress.create(localize(30014), message=message) # Download in progress
|
||
|
|
||
|
starttime = time()
|
||
|
chunk_size = 32 * 1024
|
||
|
with open(compat_path(download_path), 'wb') as image:
|
||
|
size = 0
|
||
|
while size < total_length:
|
||
|
try:
|
||
|
chunk = req.read(chunk_size)
|
||
|
except (timeout, SSLError):
|
||
|
req.close()
|
||
|
if not yesno_dialog(localize(30004), '{line1}\n{line2}'.format(line1=localize(30064),
|
||
|
line2=localize(30065))): # Could not finish dl. Try again?
|
||
|
progress.close()
|
||
|
return False
|
||
|
|
||
|
headers = {'Range': 'bytes={}-{}'.format(size, total_length)}
|
||
|
req = _http_request(url, headers=headers)
|
||
|
if req is None:
|
||
|
return None
|
||
|
continue
|
||
|
|
||
|
image.write(chunk)
|
||
|
if checksum:
|
||
|
calc_checksum.update(chunk)
|
||
|
size += len(chunk)
|
||
|
percent = int(round(size * 100 / total_length))
|
||
|
if not background and progress.iscanceled():
|
||
|
progress.close()
|
||
|
req.close()
|
||
|
return False
|
||
|
if time() - starttime > 5:
|
||
|
time_left = int(round((total_length - size) * (time() - starttime) / size))
|
||
|
prog_message = '{line1}\n{line2}'.format(
|
||
|
line1=message,
|
||
|
line2=localize(30058, mins=time_left // 60, secs=time_left % 60)) # Time remaining
|
||
|
else:
|
||
|
prog_message = message
|
||
|
|
||
|
progress.update(percent, prog_message)
|
||
|
|
||
|
if checksum and calc_checksum.hexdigest() != checksum:
|
||
|
progress.close()
|
||
|
req.close()
|
||
|
log(4, 'Download failed, checksums do not match!')
|
||
|
return False
|
||
|
|
||
|
if dl_size and stat_file(download_path).st_size() != dl_size:
|
||
|
progress.close()
|
||
|
req.close()
|
||
|
free_space = sizeof_fmt(diskspace())
|
||
|
log(4, 'Download failed, filesize does not match! Filesystem full? Remaining diskspace in temp: {}.'.format(free_space))
|
||
|
return False
|
||
|
|
||
|
progress.close()
|
||
|
req.close()
|
||
|
store('download_path', download_path)
|
||
|
return True
|
||
|
|
||
|
|
||
|
def unzip(source, destination, file_to_unzip=None, result=[]): # pylint: disable=dangerous-default-value
|
||
|
"""Unzip files to specified path"""
|
||
|
|
||
|
if not exists(destination):
|
||
|
mkdirs(destination)
|
||
|
|
||
|
from zipfile import ZipFile
|
||
|
zip_obj = ZipFile(compat_path(source))
|
||
|
for filename in zip_obj.namelist():
|
||
|
if file_to_unzip and filename != file_to_unzip:
|
||
|
continue
|
||
|
|
||
|
# Detect and remove (dangling) symlinks before extraction
|
||
|
fullname = os.path.join(destination, filename)
|
||
|
if os.path.islink(compat_path(fullname)):
|
||
|
log(3, 'Remove (dangling) symlink at {symlink}', symlink=fullname)
|
||
|
delete(fullname)
|
||
|
|
||
|
zip_obj.extract(filename, compat_path(destination))
|
||
|
result.append(True) # Pass by reference for Thread
|
||
|
|
||
|
return bool(result)
|
||
|
|
||
|
|
||
|
def system_os():
|
||
|
"""Get system platform, and remember this information"""
|
||
|
|
||
|
if hasattr(system_os, 'cached'):
|
||
|
return getattr(system_os, 'cached')
|
||
|
|
||
|
from xbmc import getCondVisibility
|
||
|
if getCondVisibility('system.platform.android'):
|
||
|
sys_name = 'Android'
|
||
|
else:
|
||
|
from platform import system
|
||
|
sys_name = system()
|
||
|
|
||
|
system_os.cached = sys_name
|
||
|
return sys_name
|
||
|
|
||
|
|
||
|
def store(name, val=None):
|
||
|
"""Store arbitrary value across functions"""
|
||
|
|
||
|
if val is not None:
|
||
|
setattr(store, name, val)
|
||
|
log(0, 'Stored {} in {}'.format(val, name))
|
||
|
return val
|
||
|
|
||
|
if not hasattr(store, name):
|
||
|
return None
|
||
|
return getattr(store, name)
|
||
|
|
||
|
|
||
|
def diskspace():
|
||
|
"""Return the free disk space available (in bytes) in temp_path."""
|
||
|
statvfs = os.statvfs(compat_path(temp_path()))
|
||
|
return statvfs.f_frsize * statvfs.f_bavail
|
||
|
|
||
|
|
||
|
def cmd_exists(cmd):
|
||
|
"""Check whether cmd exists on system."""
|
||
|
# https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
|
||
|
import subprocess
|
||
|
return subprocess.call(['type ' + cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0
|
||
|
|
||
|
|
||
|
def run_cmd(cmd, sudo=False, shell=False):
|
||
|
"""Run subprocess command and return if it succeeds as a bool"""
|
||
|
import subprocess
|
||
|
env = os.environ.copy()
|
||
|
env['LANG'] = 'C'
|
||
|
output = ''
|
||
|
success = False
|
||
|
if sudo and os.getuid() != 0 and cmd_exists('sudo'):
|
||
|
cmd.insert(0, 'sudo')
|
||
|
|
||
|
try:
|
||
|
output = to_unicode(subprocess.check_output(cmd, shell=shell, stderr=subprocess.STDOUT, env=env))
|
||
|
except subprocess.CalledProcessError as error:
|
||
|
output = to_unicode(error.output)
|
||
|
log(4, '{cmd} cmd failed.', cmd=cmd)
|
||
|
except OSError as error:
|
||
|
log(4, '{cmd} cmd doesn\'t exist. {error}', cmd=cmd, error=error)
|
||
|
else:
|
||
|
success = True
|
||
|
log(0, '{cmd} cmd executed successfully.', cmd=cmd)
|
||
|
|
||
|
if output.rstrip():
|
||
|
log(0, '{cmd} cmd output:\n{output}', cmd=cmd, output=output)
|
||
|
if from_unicode('sudo') in cmd:
|
||
|
subprocess.call(['sudo', '-k']) # reset timestamp
|
||
|
|
||
|
return {
|
||
|
'output': output,
|
||
|
'success': success
|
||
|
}
|
||
|
|
||
|
|
||
|
def sizeof_fmt(num, suffix='B'):
|
||
|
"""Return size of file in a human readable string."""
|
||
|
# https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
|
||
|
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||
|
if abs(num) < 1024.0:
|
||
|
return "%3.1f%s%s" % (num, unit, suffix)
|
||
|
num /= 1024.0
|
||
|
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||
|
|
||
|
|
||
|
def arch():
|
||
|
"""Map together, cache and return the system architecture"""
|
||
|
|
||
|
if hasattr(arch, 'cached'):
|
||
|
return getattr(arch, 'cached')
|
||
|
|
||
|
from platform import architecture, machine
|
||
|
sys_arch = machine()
|
||
|
if sys_arch == 'AMD64':
|
||
|
sys_arch_bit = architecture()[0]
|
||
|
if sys_arch_bit == '32bit':
|
||
|
sys_arch = 'x86' # else, sys_arch = AMD64
|
||
|
|
||
|
elif 'armv' in sys_arch:
|
||
|
import re
|
||
|
arm_version = re.search(r'\d+', sys_arch.split('v')[1])
|
||
|
if arm_version:
|
||
|
sys_arch = 'armv' + arm_version.group()
|
||
|
|
||
|
if sys_arch in config.ARCH_MAP:
|
||
|
sys_arch = config.ARCH_MAP[sys_arch]
|
||
|
|
||
|
log(0, 'Found system architecture {arch}', arch=sys_arch)
|
||
|
|
||
|
arch.cached = sys_arch
|
||
|
return sys_arch
|
||
|
|
||
|
|
||
|
def hardlink(src, dest):
|
||
|
"""Hardlink a file when possible, copy when needed"""
|
||
|
if exists(dest):
|
||
|
delete(dest)
|
||
|
|
||
|
try:
|
||
|
from os import link
|
||
|
link(compat_path(src), compat_path(dest))
|
||
|
except (AttributeError, OSError, ImportError):
|
||
|
return copy(src, dest)
|
||
|
log(2, "Hardlink file '{src}' to '{dest}'.", src=src, dest=dest)
|
||
|
return True
|
||
|
|
||
|
|
||
|
def remove_tree(path):
|
||
|
"""Remove an entire directory tree"""
|
||
|
from shutil import rmtree
|
||
|
rmtree(compat_path(path))
|