diff --git a/lib/inputstreamhelper/api.py b/lib/inputstreamhelper/api.py index 941f975e..0a30950e 100644 --- a/lib/inputstreamhelper/api.py +++ b/lib/inputstreamhelper/api.py @@ -3,6 +3,7 @@ """This is the actual InputStream Helper API script""" from __future__ import absolute_import, division, unicode_literals + from . import Helper from .kodiutils import ADDON, log diff --git a/lib/inputstreamhelper/config.py b/lib/inputstreamhelper/config.py index bd5ffe3b..6e0edf0d 100644 --- a/lib/inputstreamhelper/config.py +++ b/lib/inputstreamhelper/config.py @@ -3,7 +3,6 @@ """Configuration variables for inpustreamhelper""" from __future__ import absolute_import, division, unicode_literals - INPUTSTREAM_PROTOCOLS = { 'mpd': 'inputstream.adaptive', 'ism': 'inputstream.adaptive', diff --git a/lib/inputstreamhelper/kodiutils.py b/lib/inputstreamhelper/kodiutils.py index 4408e51a..548da1f6 100644 --- a/lib/inputstreamhelper/kodiutils.py +++ b/lib/inputstreamhelper/kodiutils.py @@ -3,16 +3,13 @@ """Implements Kodi Helper functions""" from __future__ import absolute_import, division, unicode_literals + from contextlib import contextmanager + import xbmc import xbmcaddon from xbmcgui import DialogProgress, DialogProgressBG - -try: # Kodi v19 or newer - from xbmcvfs import translatePath -except ImportError: # Kodi v18 and older - # pylint: disable=ungrouped-imports - from xbmc import translatePath +from xbmcvfs import translatePath from .unicodes import from_unicode, to_unicode @@ -33,18 +30,10 @@ def __init__(self): def create(self, heading, message=''): # pylint: disable=arguments-differ """Create and show a progress dialog""" - if kodi_version_major() < 19: - lines = message.split('\n', 2) - line1, line2, line3 = (lines + [None] * (3 - len(lines))) - return super(progress_dialog, self).create(heading, line1=line1, line2=line2, line3=line3) return super(progress_dialog, self).create(heading, message=message) def update(self, percent, message=''): # pylint: disable=arguments-differ """Update the progress dialog""" - if kodi_version_major() < 19: - lines = message.split('\n', 2) - line1, line2, line3 = (lines + [None] * (3 - len(lines))) - return super(progress_dialog, self).update(percent, line1=line1, line2=line2, line3=line3) return super(progress_dialog, self).update(percent, message=message) @@ -105,7 +94,8 @@ def addon_version(addon_name=None): return get_addon_info('version', addon) -def browsesingle(type, heading, shares='', mask='', useThumbs=False, treatAsFolder=False, defaultt=None): # pylint: disable=invalid-name,redefined-builtin +# pylint: disable=invalid-name,redefined-builtin,too-many-positional-arguments +def browsesingle(type, heading, shares='', mask='', useThumbs=False, treatAsFolder=False, defaultt=None): """Show a Kodi browseSingle dialog""" from xbmcgui import Dialog if not heading: @@ -127,8 +117,6 @@ def ok_dialog(heading='', message=''): from xbmcgui import Dialog if not heading: heading = ADDON.getAddonInfo('name') - if kodi_version_major() < 19: - return Dialog().ok(heading=heading, line1=message) return Dialog().ok(heading=heading, message=message) @@ -155,8 +143,6 @@ def yesno_dialog(heading='', message='', nolabel=None, yeslabel=None, autoclose= from xbmcgui import Dialog if not heading: heading = ADDON.getAddonInfo('name') - if kodi_version_major() < 19: - return Dialog().yesno(heading=heading, line1=message, nolabel=nolabel, yeslabel=yeslabel, autoclose=autoclose) return Dialog().yesno(heading=heading, message=message, nolabel=nolabel, yeslabel=yeslabel, autoclose=autoclose) diff --git a/lib/inputstreamhelper/utils.py b/lib/inputstreamhelper/utils.py index 16b6bd1b..434b879e 100644 --- a/lib/inputstreamhelper/utils.py +++ b/lib/inputstreamhelper/utils.py @@ -9,7 +9,6 @@ import struct from functools import total_ordering from socket import timeout -from ssl import SSLError from time import time from typing import NamedTuple from urllib.error import HTTPError, URLError @@ -73,26 +72,23 @@ def download_path(url): return os.path.join(temp_path(), filename) -def _http_request(url, headers=None, time_out=10): - """Perform an HTTP request and return request""" - log(0, 'Request URL: {url}', url=url) - +def _http_request(url, headers=None, time_out=30): + """Make a robust HTTP request handling redirections.""" 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 {} Error for url: {}'.format(req.getcode(), url), response=req) + response = urlopen(url, timeout=time_out) # pylint: disable=consider-using-with:w + if response.status in [301, 302, 303, 307, 308]: # Handle redirections + new_url = response.getheader('Location') + log(1, f"Redirecting to {new_url}") + return _http_request(new_url, time_out) + return response # Return the response for streaming 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? + if yesno_dialog(localize(30004), '{line1}\n{line2}'.format(line1=localize(30063), line2=localize(30065))): return _http_request(url, headers, time_out) return None - - return req + except timeout as err: + log(2, f"HTTP request timed out: {err}") + return None def http_get(url): @@ -100,7 +96,6 @@ def http_get(url): 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) @@ -108,108 +103,137 @@ def http_get(url): def http_head(url): - """Perform an HTTP HEAD request and return status code""" + """Perform an HTTP HEAD request and return status code.""" req = Request(url) req.get_method = lambda: 'HEAD' try: - resp = urlopen(req) - return resp.getcode() + with urlopen(req) as resp: + return resp.getcode() except HTTPError as exc: return exc.getcode() -def http_download(url, message=None, checksum=None, hash_alg='sha1', dl_size=None, background=False): # pylint: disable=too-many-statements +# pylint: disable=too-many-positional-arguments +def http_download(url, message=None, checksum=None, hash_alg='sha1', dl_size=None, background=False): """Makes HTTP request and displays a progress dialog on download.""" - if checksum: - from hashlib import md5, sha1 - 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 + calc_checksum = _initialize_checksum(checksum, hash_alg) + if checksum and not calc_checksum: + checksum = None - req = _http_request(url) - if req is None: + response = _http_request(url) + if response is None: return None dl_path = download_path(url) filename = os.path.basename(dl_path) - if not message: # display "downloading [filename]" + if not message: message = localize(30015, filename=filename) # Downloading file - total_length = int(req.info().get('content-length')) + total_length = int(response.info().get('content-length', 0)) 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 + dl_size = total_length - if background: - progress = bg_progress_dialog() - else: - progress = progress_dialog() + progress = _create_progress_dialog(background, message) + + success = _download_file(response, dl_path, calc_checksum, total_length, message, progress, background) + + progress.close() + response.close() + + if not success: + return False + + checksum_ok = _verify_checksum(checksum, calc_checksum) + size_ok = _verify_size(dl_size, dl_path) + + if not checksum_ok or not size_ok: + if not _handle_corrupt_file(dl_size, dl_path, checksum, calc_checksum, filename): + return False + + return dl_path + + +def _initialize_checksum(checksum, hash_alg): + if not checksum: + return None + + from hashlib import md5, sha1 + if hash_alg == 'sha1': + return sha1() + if hash_alg == 'md5': + return md5() + log(4, 'Invalid hash algorithm specified: {}'.format(hash_alg)) + return None + + +def _create_progress_dialog(background, message): + progress = bg_progress_dialog() if background else progress_dialog() progress.create(localize(30014), message=message) # Download in progress + return progress + +# pylint: disable=too-many-positional-arguments +def _download_file(response, dl_path, calc_checksum, total_length, message, progress, background): starttime = time() chunk_size = 32 * 1024 + size = 0 + with open(compat_path(dl_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 + while True: + chunk = response.read(chunk_size) + if not chunk: + break image.write(chunk) - if checksum: + if calc_checksum: calc_checksum.update(chunk) size += len(chunk) - percent = int(round(size * 100 / total_length)) + percent = int(round(size * 100 / total_length)) if total_length > 0 else 0 + if not background and progress.iscanceled(): - progress.close() - req.close() return False - if time() - starttime > 5: + + if time() - starttime > 5 and size > 0: 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 + line2=localize(30058, mins=time_left // 60, secs=time_left % 60)) else: prog_message = message progress.update(percent, prog_message) - progress.close() - req.close() + return True + + +def _verify_checksum(checksum, calc_checksum): + if not checksum: + return True + if calc_checksum: + return calc_checksum.hexdigest() == checksum + return False + - checksum_ok = (not checksum or calc_checksum.hexdigest() == checksum) - size_ok = (not dl_size or stat_file(dl_path).st_size() == dl_size) +def _verify_size(dl_size, dl_path): + if not dl_size: + return True + return stat_file(dl_path).st_size() == dl_size - if not all((checksum_ok, size_ok)): + +def _handle_corrupt_file(dl_size, dl_path, checksum, calc_checksum, filename): + log(4, 'Something may be wrong with the downloaded file.') + if checksum and calc_checksum: + log(4, 'Provided checksum: {}\nCalculated checksum: {}'.format(checksum, calc_checksum.hexdigest())) + if dl_size: free_space = sizeof_fmt(diskspace()) - log(4, 'Something may be wrong with the downloaded file.') - if not checksum_ok: - log(4, 'Provided checksum: {}\nCalculated checksum: {}'.format(checksum, calc_checksum.hexdigest())) - if not size_ok: - free_space = sizeof_fmt(diskspace()) - log(4, 'Expected filesize: {}\nReal filesize: {}\nRemaining diskspace: {}'.format(dl_size, stat_file(dl_path).st_size(), free_space)) - - if yesno_dialog(localize(30003), localize(30070, filename=filename)): # file maybe broken. Continue anyway? - log(4, 'Continuing despite possibly corrupt file!') - else: - return False + log(4, 'Expected filesize: {}\nReal filesize: {}\nRemaining diskspace: {}'.format( + dl_size, stat_file(dl_path).st_size(), free_space)) + if yesno_dialog(localize(30003), localize(30070, filename=filename)): + log(4, 'Continuing despite possibly corrupt file!') + return True - return dl_path + return False def unzip(source, destination, file_to_unzip=None, result=[]): # pylint: disable=dangerous-default-value diff --git a/lib/inputstreamhelper/widevine/arm.py b/lib/inputstreamhelper/widevine/arm.py index c8a0a49c..a0c9acbe 100644 --- a/lib/inputstreamhelper/widevine/arm.py +++ b/lib/inputstreamhelper/widevine/arm.py @@ -3,12 +3,15 @@ """Implements ARM specific widevine functions""" from __future__ import absolute_import, division, unicode_literals -import os + import json +import os from .. import config -from ..kodiutils import browsesingle, localize, log, ok_dialog, open_file, progress_dialog, yesno_dialog -from ..utils import diskspace, http_download, http_get, parse_version, sizeof_fmt, system_os, update_temp_path, userspace64 +from ..kodiutils import (browsesingle, localize, log, ok_dialog, open_file, + progress_dialog, yesno_dialog) +from ..utils import (diskspace, http_download, http_get, parse_version, + sizeof_fmt, system_os, update_temp_path, userspace64) from .arm_chromeos import ChromeOSImage from .arm_lacros import cdm_from_lacros, install_widevine_arm_lacros diff --git a/lib/inputstreamhelper/widevine/arm_chromeos.py b/lib/inputstreamhelper/widevine/arm_chromeos.py index 9a334d10..8d1621f7 100644 --- a/lib/inputstreamhelper/widevine/arm_chromeos.py +++ b/lib/inputstreamhelper/widevine/arm_chromeos.py @@ -3,13 +3,14 @@ """Implements a class with methods related to the Chrome OS image""" from __future__ import absolute_import, division, unicode_literals + import os +from io import UnsupportedOperation from struct import calcsize, unpack from zipfile import ZipFile -from io import UnsupportedOperation -from ..kodiutils import exists, localize, log, mkdirs from .. import config +from ..kodiutils import exists, localize, log, mkdirs from ..unicodes import compat_path diff --git a/lib/inputstreamhelper/widevine/repo.py b/lib/inputstreamhelper/widevine/repo.py index 24cfd8f2..5cd4744e 100644 --- a/lib/inputstreamhelper/widevine/repo.py +++ b/lib/inputstreamhelper/widevine/repo.py @@ -19,7 +19,10 @@ def cdm_from_repo(): def widevines_available_from_repo(): """Returns all available Widevine CDM versions and urls from Google's library CDM repository""" - cdm_versions = http_get(config.WIDEVINE_VERSIONS_URL).strip('\n').split('\n') + cdm_versions = http_get(config.WIDEVINE_VERSIONS_URL) + log(0, f"Available Widevine versions from repo: {cdm_versions}") + cdm_versions = cdm_versions.strip('\n').split('\n') + log(0, f"Available Widevine versions from repo: {cdm_versions}") try: cdm_os = config.WIDEVINE_OS_MAP[system_os()] cdm_arch = config.WIDEVINE_ARCH_MAP_REPO[arch()] @@ -32,6 +35,8 @@ def widevines_available_from_repo(): http_status = http_head(cdm_url) if http_status == 200: available_cdms.append({'version': cdm_version, 'url': cdm_url}) + continue + log(2, f'Widevine version {cdm_version} is not available from {cdm_url}') if not available_cdms: log(4, "could not find any available cdm in repo") diff --git a/lib/inputstreamhelper/widevine/widevine.py b/lib/inputstreamhelper/widevine/widevine.py index a480fd4f..0204c69f 100644 --- a/lib/inputstreamhelper/widevine/widevine.py +++ b/lib/inputstreamhelper/widevine/widevine.py @@ -40,7 +40,7 @@ def widevine_eula(): cdm_arch = config.WIDEVINE_ARCH_MAP_REPO[arch()] else: # Grab the license from the x86 files log(0, 'Acquiring Widevine EULA from x86 files.') - cdm_version = '4.10.2830.0' # fine to hardcode as it's only used for the EULA + cdm_version = '4.10.2830.0' # fine to hardcode as it's only used for the EULA cdm_os = 'mac' cdm_arch = 'x64' diff --git a/tests/xbmcgui.py b/tests/xbmcgui.py index 4bad5527..6405a8ae 100644 --- a/tests/xbmcgui.py +++ b/tests/xbmcgui.py @@ -3,10 +3,12 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """This file implements the Kodi xbmcgui module, either using stubs or alternative functionality""" -# flake8: noqa: FI14; pylint: disable=invalid-name,super-on-old-class,too-few-public-methods,too-many-arguments,unused-argument,useless-super-delegation +# flake8: noqa: FI14; pylint: disable=invalid-name,super-on-old-class,too-few-public-methods,too-many-arguments,unused-argument,useless-super-delegation,too-many-positional-arguments from __future__ import absolute_import, division, print_function + import sys + from xbmcextra import kodi_to_ansi