Skip to content

CI fixes #578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions lib/inputstreamhelper/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion lib/inputstreamhelper/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""Configuration variables for inpustreamhelper"""
from __future__ import absolute_import, division, unicode_literals


INPUTSTREAM_PROTOCOLS = {
'mpd': 'inputstream.adaptive',
'ism': 'inputstream.adaptive',
Expand Down
24 changes: 5 additions & 19 deletions lib/inputstreamhelper/kodiutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)


Expand Down Expand Up @@ -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:
Expand All @@ -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)


Expand All @@ -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)


Expand Down
180 changes: 102 additions & 78 deletions lib/inputstreamhelper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -73,143 +72,168 @@ 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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the headers parameter would be unused after this change

"""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?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please leave this comment in, so one doesn't have to look up what message 30065 says.

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):
"""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("utf-8")


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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to avoid while True. To make sure we can never get stuck in an infinite loop, I would stick with while size < total_length

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, keep the comment. I don't want to have to look up what message 30058 is.

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
Expand Down
9 changes: 6 additions & 3 deletions lib/inputstreamhelper/widevine/arm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions lib/inputstreamhelper/widevine/arm_chromeos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
7 changes: 6 additions & 1 deletion lib/inputstreamhelper/widevine/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a bit much, even in a debug log

try:
cdm_os = config.WIDEVINE_OS_MAP[system_os()]
cdm_arch = config.WIDEVINE_ARCH_MAP_REPO[arch()]
Expand All @@ -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")
Expand Down
Loading
Loading