487 lines
20 KiB
Python
487 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
# MIT License (see LICENSE.txt or https://opensource.org/licenses/MIT)
|
|
"""Implements the main InputStream Helper class"""
|
|
|
|
from __future__ import absolute_import, division, unicode_literals
|
|
import os
|
|
|
|
from . import config
|
|
from .kodiutils import (addon_version, delete, exists, get_proxies, get_setting, get_setting_bool, get_setting_float, get_setting_int, jsonrpc,
|
|
kodi_to_ascii, kodi_version, listdir, localize, log, notification, ok_dialog, progress_dialog, select_dialog,
|
|
set_setting, set_setting_bool, textviewer, translate_path, yesno_dialog)
|
|
from .utils import arch, http_download, remove_tree, run_cmd, store, system_os, temp_path, unzip
|
|
from .widevine.arm import install_widevine_arm, unmount
|
|
from .widevine.widevine import (backup_path, has_widevinecdm, ia_cdm_path, install_cdm_from_backup, latest_widevine_version,
|
|
load_widevine_config, missing_widevine_libs, widevine_config_path, widevine_eula, widevinecdm_path)
|
|
from .unicodes import compat_path
|
|
|
|
# NOTE: Work around issue caused by platform still using os.popen()
|
|
# This helps to survive 'IOError: [Errno 10] No child processes'
|
|
if hasattr(os, 'popen'):
|
|
del os.popen
|
|
|
|
|
|
class InputStreamException(Exception):
|
|
"""Stub Exception"""
|
|
|
|
|
|
def cleanup_decorator(func):
|
|
"""Decorator which runs cleanup before and after a function"""
|
|
|
|
def clean_before_after(self, *args, **kwargs): # pylint: disable=missing-docstring
|
|
# pylint only complains about a missing docstring on py2.7?
|
|
self.cleanup()
|
|
result = func(self, *args, **kwargs)
|
|
self.cleanup()
|
|
return result
|
|
return clean_before_after
|
|
|
|
|
|
class Helper:
|
|
"""The main InputStream Helper class"""
|
|
|
|
def __init__(self, protocol, drm=None):
|
|
"""Initialize InputStream Helper class"""
|
|
|
|
self.protocol = protocol
|
|
self.drm = drm
|
|
|
|
from platform import uname
|
|
log(0, 'Platform information: {uname}', uname=uname())
|
|
|
|
if self.protocol not in config.INPUTSTREAM_PROTOCOLS:
|
|
raise InputStreamException('UnsupportedProtocol')
|
|
|
|
self.inputstream_addon = config.INPUTSTREAM_PROTOCOLS[self.protocol]
|
|
|
|
if self.drm:
|
|
if self.drm not in config.DRM_SCHEMES:
|
|
raise InputStreamException('UnsupportedDRMScheme')
|
|
|
|
self.drm = config.DRM_SCHEMES[drm]
|
|
|
|
# Add proxy support to HTTP requests
|
|
proxies = get_proxies()
|
|
if proxies:
|
|
try: # Python 3
|
|
from urllib.request import build_opener, install_opener, ProxyHandler
|
|
except ImportError: # Python 2
|
|
from urllib2 import build_opener, install_opener, ProxyHandler
|
|
install_opener(build_opener(ProxyHandler(proxies)))
|
|
|
|
def __repr__(self):
|
|
"""String representation of Helper class"""
|
|
return 'Helper({protocol}, drm={drm})'.format(protocol=self.protocol, drm=self.drm)
|
|
|
|
@staticmethod
|
|
def disable():
|
|
"""Disable plugin"""
|
|
if not get_setting_bool('disabled', False):
|
|
set_setting_bool('disabled', True)
|
|
|
|
@staticmethod
|
|
def enable():
|
|
"""Enable plugin"""
|
|
if get_setting('disabled', False):
|
|
set_setting_bool('disabled', False)
|
|
|
|
def _inputstream_version(self):
|
|
"""Return the requested inputstream version"""
|
|
from xbmcaddon import Addon
|
|
try:
|
|
addon = Addon(self.inputstream_addon)
|
|
except RuntimeError:
|
|
return None
|
|
|
|
from .unicodes import to_unicode
|
|
return to_unicode(addon.getAddonInfo('version'))
|
|
|
|
@staticmethod
|
|
def _get_lib_version(path):
|
|
if not path or not exists(path):
|
|
return '(Not found)'
|
|
import re
|
|
with open(compat_path(path), 'rb') as library:
|
|
match = re.search(br'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+', library.read())
|
|
if not match:
|
|
return '(Undetected)'
|
|
from .unicodes import to_unicode
|
|
return to_unicode(match.group(0))
|
|
|
|
def _has_inputstream(self):
|
|
"""Checks if selected InputStream add-on is installed."""
|
|
data = jsonrpc(method='Addons.GetAddonDetails', params=dict(addonid=self.inputstream_addon))
|
|
if 'error' in data:
|
|
log(3, '{addon} is not installed.', addon=self.inputstream_addon)
|
|
return False
|
|
|
|
log(0, '{addon} is installed.', addon=self.inputstream_addon)
|
|
return True
|
|
|
|
def _inputstream_enabled(self):
|
|
"""Returns whether selected InputStream add-on is enabled.."""
|
|
data = jsonrpc(method='Addons.GetAddonDetails', params=dict(addonid=self.inputstream_addon, properties=['enabled']))
|
|
if data.get('result', {}).get('addon', {}).get('enabled'):
|
|
log(0, '{addon} {version} is enabled.', addon=self.inputstream_addon, version=self._inputstream_version())
|
|
return True
|
|
|
|
log(3, '{addon} is disabled.', addon=self.inputstream_addon)
|
|
return False
|
|
|
|
def _enable_inputstream(self):
|
|
"""Enables selected InputStream add-on."""
|
|
data = jsonrpc(method='Addons.SetAddonEnabled', params=dict(addonid=self.inputstream_addon, enabled=True))
|
|
if 'error' in data:
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
def _supports_widevine():
|
|
"""Checks if Widevine is supported on the architecture/operating system/Kodi version."""
|
|
if arch() not in config.WIDEVINE_SUPPORTED_ARCHS:
|
|
log(4, 'Unsupported Widevine architecture found: {arch}', arch=arch())
|
|
ok_dialog(localize(30004), localize(30007, arch=arch())) # Widevine not available on this architecture
|
|
return False
|
|
|
|
if arch() == 'arm64' and system_os() != 'Android':
|
|
import struct
|
|
if struct.calcsize('P') * 8 == 64:
|
|
log(4, 'Unsupported 64-bit userspace found. User needs 32-bit userspace on {arch}', arch=arch())
|
|
ok_dialog(localize(30004), localize(30039)) # Widevine not available on ARM64
|
|
return False
|
|
|
|
if system_os() not in config.WIDEVINE_SUPPORTED_OS:
|
|
log(4, 'Unsupported Widevine OS found: {os}', os=system_os())
|
|
ok_dialog(localize(30004), localize(30011, os=system_os())) # Operating system not supported by Widevine
|
|
return False
|
|
|
|
from distutils.version import LooseVersion # pylint: disable=import-error,no-name-in-module,useless-suppression
|
|
if LooseVersion(config.WIDEVINE_MINIMUM_KODI_VERSION[system_os()]) > LooseVersion(kodi_version()):
|
|
log(4, 'Unsupported Kodi version for Widevine: {version}', version=kodi_version())
|
|
ok_dialog(localize(30004), localize(30010, version=config.WIDEVINE_MINIMUM_KODI_VERSION[system_os()])) # Kodi too old
|
|
return False
|
|
|
|
if 'WindowsApps' in translate_path('special://xbmcbin/'): # uwp is not supported
|
|
log(4, 'Unsupported UWP Kodi version detected.')
|
|
ok_dialog(localize(30004), localize(30012)) # Windows Store Kodi falls short
|
|
return False
|
|
|
|
return True
|
|
|
|
@staticmethod
|
|
def _install_widevine_x86(bpath):
|
|
"""Install Widevine CDM on x86 based architectures."""
|
|
cdm_version = latest_widevine_version()
|
|
|
|
if not store('download_path'):
|
|
cdm_os = config.WIDEVINE_OS_MAP[system_os()]
|
|
cdm_arch = config.WIDEVINE_ARCH_MAP_X86[arch()]
|
|
url = config.WIDEVINE_DOWNLOAD_URL.format(version=cdm_version, os=cdm_os, arch=cdm_arch)
|
|
|
|
downloaded = http_download(url)
|
|
else:
|
|
downloaded = True
|
|
|
|
if downloaded:
|
|
progress = progress_dialog()
|
|
progress.create(heading=localize(30043), message=localize(30044)) # Extracting Widevine CDM
|
|
unzip(store('download_path'), os.path.join(bpath, cdm_version, ''))
|
|
|
|
return (progress, cdm_version)
|
|
|
|
return False
|
|
|
|
def install_and_finish(self, progress, version):
|
|
"""Installs the cdm from backup and runs checks"""
|
|
|
|
progress.update(97, message=localize(30049)) # Installing Widevine CDM
|
|
install_cdm_from_backup(version)
|
|
|
|
progress.update(98, message=localize(30050)) # Finishing
|
|
if has_widevinecdm():
|
|
wv_check = self._check_widevine()
|
|
if wv_check:
|
|
progress.update(100, message=localize(30051)) # Widevine CDM successfully installed.
|
|
notification(localize(30037), localize(30051)) # Success! Widevine CDM successfully installed.
|
|
progress.close()
|
|
return wv_check
|
|
|
|
progress.close()
|
|
return False
|
|
|
|
@cleanup_decorator
|
|
def install_widevine(self):
|
|
"""Wrapper function that calls Widevine installer method depending on architecture"""
|
|
if not self._supports_widevine():
|
|
return False
|
|
|
|
if not widevine_eula():
|
|
return False
|
|
|
|
if 'x86' in arch():
|
|
result = self._install_widevine_x86(backup_path())
|
|
else:
|
|
result = install_widevine_arm(backup_path())
|
|
if not result:
|
|
return result
|
|
|
|
if self.install_and_finish(*result):
|
|
from time import time
|
|
set_setting('last_check', time())
|
|
return True
|
|
|
|
ok_dialog(localize(30004), localize(30005)) # An error occurred
|
|
return False
|
|
|
|
@staticmethod
|
|
def remove_widevine():
|
|
"""Removes Widevine CDM"""
|
|
if has_widevinecdm():
|
|
widevinecdm = widevinecdm_path()
|
|
log(0, 'Removed Widevine CDM at {path}', path=widevinecdm)
|
|
delete(widevinecdm)
|
|
notification(localize(30037), localize(30052)) # Success! Widevine successfully removed.
|
|
set_setting('last_modified', '0.0')
|
|
return True
|
|
notification(localize(30004), localize(30053)) # Error. Widevine CDM not found.
|
|
return False
|
|
|
|
@staticmethod
|
|
def _first_run():
|
|
"""Check if this add-on version is running for the first time"""
|
|
|
|
# Get versions
|
|
settings_version = get_setting('version', '0.3.4') # settings_version didn't exist in version 0.3.4 and older
|
|
|
|
# Compare versions
|
|
from distutils.version import LooseVersion # pylint: disable=import-error,no-name-in-module,useless-suppression
|
|
if LooseVersion(addon_version()) > LooseVersion(settings_version):
|
|
# New version found, save addon_version to settings
|
|
set_setting('version', addon_version())
|
|
log(2, 'InputStreamHelper version {version} is running for the first time', version=addon_version())
|
|
return True
|
|
return False
|
|
|
|
def _update_widevine(self):
|
|
"""Prompts user to upgrade Widevine CDM when a newer version is available."""
|
|
from time import localtime, strftime, time
|
|
|
|
last_check = get_setting_float('last_check', 0.0)
|
|
if last_check and not self._first_run():
|
|
if last_check + 3600 * 24 * get_setting_int('update_frequency', 14) >= time():
|
|
log(2, 'Widevine update check was made on {date}', date=strftime('%Y-%m-%d %H:%M', localtime(last_check)))
|
|
return
|
|
|
|
wv_config = load_widevine_config()
|
|
if not wv_config:
|
|
log(3, 'Widevine config missing. Could not determine current version, forcing update.')
|
|
current_version = '0'
|
|
elif 'x86' in arch():
|
|
component = 'Widevine CDM'
|
|
current_version = wv_config['version']
|
|
else:
|
|
component = 'Chrome OS'
|
|
current_version = wv_config['version']
|
|
|
|
latest_version = latest_widevine_version()
|
|
if not latest_version:
|
|
log(3, 'Updating widevine failed. Could not determine latest version.')
|
|
return
|
|
|
|
log(0, 'Latest {component} version is {version}', component=component, version=latest_version)
|
|
log(0, 'Current {component} version installed is {version}', component=component, version=current_version)
|
|
|
|
from distutils.version import LooseVersion # pylint: disable=import-error,no-name-in-module,useless-suppression
|
|
if LooseVersion(latest_version) > LooseVersion(current_version):
|
|
log(2, 'There is an update available for {component}', component=component)
|
|
if yesno_dialog(localize(30040), localize(30033), nolabel=localize(30028), yeslabel=localize(30034)):
|
|
self.install_widevine()
|
|
else:
|
|
log(3, 'User declined to update {component}.', component=component)
|
|
else:
|
|
set_setting('last_check', time())
|
|
log(0, 'User is on the latest available {component} version.', component=component)
|
|
|
|
def _check_widevine(self):
|
|
"""Checks that all Widevine components are installed and available."""
|
|
if system_os() == 'Android': # no checks needed for Android
|
|
return True
|
|
|
|
if not exists(widevine_config_path()):
|
|
log(4, 'Widevine or Chrome OS recovery.json is missing. Reinstall is required.')
|
|
ok_dialog(localize(30001), localize(30031)) # An update of Widevine is required
|
|
return self.install_widevine()
|
|
|
|
if 'x86' in arch(): # check that widevine arch matches system arch
|
|
wv_config = load_widevine_config()
|
|
if config.WIDEVINE_ARCH_MAP_X86[arch()] != wv_config['arch']:
|
|
log(4, 'Widevine/system arch mismatch. Reinstall is required.')
|
|
ok_dialog(localize(30001), localize(30031)) # An update of Widevine is required
|
|
return self.install_widevine()
|
|
|
|
if missing_widevine_libs():
|
|
ok_dialog(localize(30004), localize(30032, libs=', '.join(missing_widevine_libs()))) # Missing libraries
|
|
return False
|
|
|
|
self._update_widevine()
|
|
return True
|
|
|
|
@staticmethod
|
|
def cleanup():
|
|
"""Clean up function after Widevine CDM installation"""
|
|
unmount()
|
|
if store('attached_loop_dev'):
|
|
cmd = ['losetup', '-d', store('loop_dev')]
|
|
unattach_output = run_cmd(cmd, sudo=True)
|
|
if unattach_output['success']:
|
|
store('loop_dev', False)
|
|
store('attached_loop_dev', False)
|
|
if store('modprobe_loop'):
|
|
notification(localize(30035), localize(30036)) # Unload by hand in CLI
|
|
if not has_widevinecdm():
|
|
remove_tree(ia_cdm_path())
|
|
|
|
remove_tree(temp_path())
|
|
return True
|
|
|
|
def _supports_hls(self):
|
|
"""Return if HLS support is available in inputstream.adaptive."""
|
|
from distutils.version import LooseVersion # pylint: disable=import-error,no-name-in-module,useless-suppression
|
|
if LooseVersion(self._inputstream_version()) >= LooseVersion(config.HLS_MINIMUM_IA_VERSION):
|
|
return True
|
|
|
|
log(3, 'HLS is unsupported on {addon} version {version}', addon=self.inputstream_addon, version=self._inputstream_version())
|
|
return False
|
|
|
|
def _check_drm(self):
|
|
"""Main function for ensuring that specified DRM system is installed and available."""
|
|
if not self.drm or self.inputstream_addon != 'inputstream.adaptive':
|
|
return True
|
|
|
|
if self.drm != 'widevine':
|
|
return True
|
|
|
|
if has_widevinecdm():
|
|
return self._check_widevine()
|
|
|
|
if yesno_dialog(localize(30041), localize(30002), nolabel=localize(30028), yeslabel=localize(30038)): # Widevine required
|
|
return self.install_widevine()
|
|
|
|
return False
|
|
|
|
def _install_inputstream(self):
|
|
"""Install inputstream addon."""
|
|
from xbmc import executebuiltin
|
|
from xbmcaddon import Addon
|
|
try:
|
|
# See if there's an installed repo that has it
|
|
executebuiltin('InstallAddon({})'.format(self.inputstream_addon), wait=True)
|
|
|
|
# Check if InputStream add-on exists!
|
|
Addon('{}'.format(self.inputstream_addon))
|
|
|
|
log(0, 'InputStream add-on installed from repo.')
|
|
return True
|
|
except RuntimeError:
|
|
log(3, 'InputStream add-on not installed.')
|
|
return False
|
|
|
|
def check_inputstream(self):
|
|
"""Main function. Ensures that all components are available for InputStream add-on playback."""
|
|
if get_setting_bool('disabled', False): # blindly return True if helper has been disabled
|
|
log(3, 'InputStreamHelper is disabled in its settings.xml.')
|
|
return True
|
|
if self.drm == 'widevine' and not self._supports_widevine():
|
|
return False
|
|
if not self._has_inputstream():
|
|
# Try to install InputStream add-on
|
|
if not self._install_inputstream():
|
|
ok_dialog(localize(30004), localize(30008, addon=self.inputstream_addon)) # inputstream is missing on system
|
|
return False
|
|
elif not self._inputstream_enabled():
|
|
ret = yesno_dialog(localize(30001), localize(30009, addon=self.inputstream_addon)) # inputstream is disabled
|
|
if not ret:
|
|
return False
|
|
self._enable_inputstream()
|
|
log(0, '{addon} {version} is installed and enabled.', addon=self.inputstream_addon, version=self._inputstream_version())
|
|
|
|
if self.protocol == 'hls' and not self._supports_hls():
|
|
ok_dialog(localize(30004), # HLS Minimum version is needed
|
|
localize(30017, addon=self.inputstream_addon, version=config.HLS_MINIMUM_IA_VERSION))
|
|
return False
|
|
|
|
return self._check_drm()
|
|
|
|
def info_dialog(self):
|
|
""" Show an Info box with useful info e.g. for bug reports"""
|
|
text = localize(30800, version=kodi_version(), system=system_os(), arch=arch()) + '\n' # Kodi information
|
|
text += '\n'
|
|
|
|
disabled_str = ' ({disabled})'.format(disabled=localize(30054))
|
|
ishelper_state = disabled_str if get_setting_bool('disabled', False) else ''
|
|
istream_state = disabled_str if not self._inputstream_enabled() else ''
|
|
text += localize(30810, version=addon_version(), state=ishelper_state) + '\n'
|
|
text += localize(30811, version=self._inputstream_version(), state=istream_state) + '\n'
|
|
text += '\n'
|
|
|
|
if system_os() == 'Android':
|
|
text += localize(30820) + '\n'
|
|
else:
|
|
from time import localtime, strftime
|
|
if get_setting_float('last_modified', 0.0):
|
|
wv_updated = strftime('%Y-%m-%d %H:%M', localtime(get_setting_float('last_modified', 0.0)))
|
|
else:
|
|
wv_updated = 'Never'
|
|
text += localize(30821, version=self._get_lib_version(widevinecdm_path()), date=wv_updated) + '\n'
|
|
if arch() in ('arm', 'arm64'): # Chrome OS version
|
|
wv_cfg = load_widevine_config()
|
|
if wv_cfg:
|
|
text += localize(30822, name=wv_cfg['hwidmatch'].split()[0].lstrip('^'), version=wv_cfg['version']) + '\n'
|
|
if get_setting_float('last_check', 0.0):
|
|
wv_check = strftime('%Y-%m-%d %H:%M', localtime(get_setting_float('last_check', 0.0)))
|
|
else:
|
|
wv_check = 'Never'
|
|
text += localize(30823, date=wv_check) + '\n'
|
|
text += localize(30824, path=ia_cdm_path()) + '\n'
|
|
|
|
text += '\n'
|
|
|
|
text += localize(30830, url=config.SHORT_ISSUE_URL) # Report issues
|
|
|
|
log(2, '\n{info}'.format(info=kodi_to_ascii(text)))
|
|
textviewer(localize(30901), text)
|
|
|
|
def rollback_libwv(self):
|
|
"""Rollback lib to a version specified by the user"""
|
|
bpath = backup_path()
|
|
versions = listdir(bpath)
|
|
|
|
# Return if Widevine is not installed
|
|
if not exists(widevine_config_path()):
|
|
notification(localize(30004), localize(30041))
|
|
return
|
|
|
|
installed_version = load_widevine_config()['version']
|
|
del versions[versions.index(installed_version)]
|
|
|
|
if 'x86' in arch():
|
|
show_versions = versions
|
|
else:
|
|
show_versions = []
|
|
|
|
for version in versions:
|
|
lib_version = self._get_lib_version(os.path.join(bpath, version, config.WIDEVINE_CDM_FILENAME[system_os()]))
|
|
show_versions.append('{} ({})'.format(lib_version, version))
|
|
|
|
if not show_versions:
|
|
notification(localize(30004), localize(30056))
|
|
return
|
|
|
|
version = select_dialog(localize(30057), show_versions)
|
|
if version != -1:
|
|
log(0, 'Rollback to version {version}', version=versions[version])
|
|
install_cdm_from_backup(versions[version])
|
|
notification(localize(30037), localize(30051)) # Success! Widevine successfully installed.
|
|
|
|
return
|