astrXbian/.install/.kodi/addons/script.module.inputstreamhe.../lib/inputstreamhelper/utils.py

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))