From ba22c31e1d48904835def7b89f3fac322a7bc1de Mon Sep 17 00:00:00 2001 From: mediaminister Date: Tue, 4 Nov 2025 09:39:06 +0000 Subject: [PATCH] [script.module.inputstreamhelper] 0.8.4 --- script.module.inputstreamhelper/README.md | 3 + script.module.inputstreamhelper/addon.xml | 5 +- .../lib/inputstreamhelper/__init__.py | 17 +- .../lib/inputstreamhelper/config.py | 4 - .../lib/inputstreamhelper/unsquash.py | 442 ------------------ .../lib/inputstreamhelper/widevine/arm.py | 9 - .../widevine/arm_chromeos.py | 2 +- .../inputstreamhelper/widevine/arm_lacros.py | 76 --- .../inputstreamhelper/widevine/widevine.py | 11 +- .../resource.language.en_gb/strings.po | 4 - 10 files changed, 17 insertions(+), 556 deletions(-) delete mode 100644 script.module.inputstreamhelper/lib/inputstreamhelper/unsquash.py delete mode 100644 script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm_lacros.py diff --git a/script.module.inputstreamhelper/README.md b/script.module.inputstreamhelper/README.md index efb55119c..cb9284e12 100644 --- a/script.module.inputstreamhelper/README.md +++ b/script.module.inputstreamhelper/README.md @@ -90,6 +90,9 @@ Please report any issues or bug reports on the [GitHub Issues](https://github.co This module is licensed under the **The MIT License**. Please see the [LICENSE.txt](LICENSE.txt) file for details. ## Releases +### v0.8.4 (2025-11-04) +- Fix Widevine CDM installation on ARM hardware (@mediaminister) + ### v0.8.3 (2025-10-10) - Fix removing older Widevine CDM backups (@mediaminister) diff --git a/script.module.inputstreamhelper/addon.xml b/script.module.inputstreamhelper/addon.xml index 5e6d22042..7c0e521b5 100644 --- a/script.module.inputstreamhelper/addon.xml +++ b/script.module.inputstreamhelper/addon.xml @@ -1,5 +1,5 @@ - + @@ -27,6 +27,9 @@ Простой модуль для Kodi, который облегчает жизнь разработчикам дополнений, с использованием InputStream дополнений и воспроизведения DRM контента. En enkel Kodi-modul som underlättar livet för tilläggsutvecklare som förlitar sig på InputStream-baserade tillägg och DRM-uppspelning. +v0.8.4 (2025-11-04) +- Fix Widevine CDM installation on ARM hardware + v0.8.3 (2025-10-10) - Fix removing older Widevine CDM backups diff --git a/script.module.inputstreamhelper/lib/inputstreamhelper/__init__.py b/script.module.inputstreamhelper/lib/inputstreamhelper/__init__.py index ced5f50be..383f99383 100644 --- a/script.module.inputstreamhelper/lib/inputstreamhelper/__init__.py +++ b/script.module.inputstreamhelper/lib/inputstreamhelper/__init__.py @@ -9,8 +9,7 @@ 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, download_path, http_download, parse_version, remove_tree, system_os, temp_path, unzip, userspace64 -from .widevine.arm import dl_extract_widevine_chromeos, extract_widevine_chromeos, install_widevine_arm -from .widevine.arm_lacros import cdm_from_lacros +from .widevine.arm import dl_extract_widevine_chromeos, extract_widevine_chromeos, install_widevine_arm_chromeos 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, @@ -227,7 +226,7 @@ def install_widevine(self, choose_version=False): else: if choose_version: log(1, "Choosing a version to install is only implemented if the lib is found in googles repo.") - result = install_widevine_arm(backup_path()) + result = install_widevine_arm_chromeos(backup_path()) if not result: return result @@ -296,7 +295,7 @@ def _first_run(): @staticmethod def get_current_wv(): - """Returns which component is used (widevine/chromeos/lacros) and the current version""" + """Returns which component is used (widevine/chromeos) and the current version""" wv_config = load_widevine_config() component = 'Widevine CDM' current_version = '0' @@ -305,12 +304,6 @@ def get_current_wv(): log(3, 'Widevine config missing. Could not determine current version, forcing update.') elif cdm_from_repo(): current_version = wv_config['version'] - elif cdm_from_lacros(): - component = 'Lacros image' - try: - current_version = wv_config['img_version'] # if lib was installed from chromeos image, there is no img_version - except KeyError: - pass else: component = 'Chrome OS' current_version = wv_config['version'] @@ -481,9 +474,7 @@ def info_dialog(self): text += localize(30821, version=self._get_lib_version(widevinecdm_path()), date=wv_updated) + '\n' if not cdm_from_repo(): wv_cfg = load_widevine_config() - if wv_cfg and cdm_from_lacros(): # Lacros image version - text += localize(30825, image="Lacros", version=wv_cfg['img_version']) + '\n' - elif wv_cfg: # Chrome OS version + if wv_cfg: # Chrome OS version 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))) diff --git a/script.module.inputstreamhelper/lib/inputstreamhelper/config.py b/script.module.inputstreamhelper/lib/inputstreamhelper/config.py index fe5c911a7..48ef8a079 100644 --- a/script.module.inputstreamhelper/lib/inputstreamhelper/config.py +++ b/script.module.inputstreamhelper/lib/inputstreamhelper/config.py @@ -111,10 +111,6 @@ CHROMEOS_BLOCK_SIZE = 512 -LACROS_DOWNLOAD_URL = "https://gsdview.appspot.com/chromeos-localmirror/distfiles/chromeos-lacros-{arch}-squash-zstd-{version}" - -LACROS_LATEST = "https://chromiumdash.appspot.com/fetch_releases?channel=Stable&platform=Lacros&num=1" - MINIMUM_INPUTSTREAM_VERSION_ARM64 = { 'inputstream.adaptive': '20.3.5', } diff --git a/script.module.inputstreamhelper/lib/inputstreamhelper/unsquash.py b/script.module.inputstreamhelper/lib/inputstreamhelper/unsquash.py deleted file mode 100644 index eb5fca1f4..000000000 --- a/script.module.inputstreamhelper/lib/inputstreamhelper/unsquash.py +++ /dev/null @@ -1,442 +0,0 @@ -# -*- coding: utf-8 -*- -# MIT License (see LICENSE.txt or https://opensource.org/licenses/MIT) -""" -Minimal implementation of Squashfs for extracting files from an image. - -Information sourced from: -https://dr-emann.github.io/squashfs/ -https://github.com/plougher/squashfs-tools/blob/master/squashfs-tools/squashfs_fs.h - -Assumptions made: -- Zstd is used for compression. -- Directory table consists of only one metadata block. -- There is only one file with the specific name i.e. no file of the same name in another directory. -- We only need to read inodes of basic files. -""" - -import os - -from ctypes import CDLL, c_void_p, c_size_t, create_string_buffer -from ctypes.util import find_library - -from struct import unpack, calcsize -from dataclasses import dataclass -from math import log2, ceil - -from .kodiutils import log - - -class ZstdDecompressor: # pylint: disable=too-few-public-methods - """ - zstdandard decompressor class - - It's a class to avoid having to load the zstd library for every decompression. - """ - def __init__(self): - libzstd = CDLL(find_library("zstd")) - self.zstddecomp = libzstd.ZSTD_decompress - self.zstddecomp.restype = c_size_t - self.zstddecomp.argtypes = (c_void_p, c_size_t, c_void_p, c_size_t) - self.iserror = libzstd.ZSTD_isError - - def decompress(self, comp_data, comp_size, outsize=8 * 2 ** 10): - """main function, decompresses binary string """ - if len(comp_data) != comp_size: - raise IOError("Decompression failed! Length of compressed data doesn't match given size.") - - dest = create_string_buffer(outsize) - - actual_outsize = self.zstddecomp(dest, len(dest), comp_data, len(comp_data)) - if self.iserror(actual_outsize): - raise IOError(f"Decompression failed! Error code: {actual_outsize}") - return dest[:actual_outsize] # outsize is always a multiple of 8K, but real size may be smaller - - -@dataclass(frozen=True) -class SBlk: # pylint: disable=too-many-instance-attributes - """superblock as dataclass, does some checks after initialization""" - s_magic: int - inodes: int - mkfs_time: int - block_size: int - fragments: int - compression: int - block_log: int - flags: int - no_ids: int - s_major: int - s_minor: int - root_inode: int - bytes_used: int - id_table_start: int - xattr_id_table_start: int - inode_table_start: int - directory_table_start: int - fragment_table_start: int - lookup_table_start: int - - def __post_init__(self): - """Some sanity checks""" - squashfs_magic = 0x73717368 # Has to be present in every valid squashfs image - if self.s_magic != squashfs_magic: - raise IOError("Squashfs magic doesn't match!") - - if log2(self.block_size) != self.block_log: - raise IOError("block_size and block_log do not match!") - - if bool(self.flags & 0x0004): - raise IOError("Check flag should always be unset!") - - if self.s_major != 4 or self.s_minor != 0: - raise IOError("Unsupported squashfs version!") - - if self.compression != 6: - raise IOError("Image is not compressed using zstd!") - - -@dataclass(frozen=True) -class MetaDataHeader: - """ - header of metadata blocks. - - Most things are contained in metadata blocks, including: - - Compression options - - directory table - - fragment table - - file inodes - """ - compressed: bool - size: int - - -@dataclass(frozen=True) -class InodeHeader: - """squashfs_base_inode_header dataclass""" - inode_type: int - mode: int - uid: int - guid: int - mtime: int - inode_number: int - - -@dataclass(frozen=True) -class BasicFileInode: - """ - This is squashfs_reg_inode_header, but without the base inode header part - """ - start_block: int - fragment: int - offset: int - file_size: int - block_list: tuple # once we remove support for python below 3.9 this can be: tuple[int] - - -@dataclass(frozen=True) -class DirectoryHeader: - """squashfs_dir_header dataclass""" - count: int - start_block: int - inode_number: int - - -@dataclass(frozen=True) -class DirectoryEntry: - """ - Directory entry dataclass. - - This is squashfs_dir_entry in the squashfs-tools source code, - but there "itype" is called "type" and "name_size" is just "size". - - Implements __len__, giving the number of bytes of the whole entry. - """ - offset: int - inode_number: int - itype: int - name_size: int # name is 1 byte longer than given in name_size - name: bytes - - def __len__(self): - """the first four entries are 2 bytes each. name is actually one byte longer than given in name_size""" - return 8 + 1 + self.name_size - - -@dataclass(frozen=True) -class FragmentBlockEntry: - """squashfs_fragment_entry dataclass""" - start_block: int - size: int - unused: int # This field has no meaning - - -class SquashFs: - """ - Main class to handle a squashfs image, find and extract files from it. - """ - def __init__(self, fpath): - self.zdecomp = ZstdDecompressor() - self.imfile = open(fpath, "rb") # pylint: disable=consider-using-with # we have our own context manager - self.sblk = self._get_sblk() - self.frag_entries = self._get_fragment_table() - log(0, "squashfs image initialized") - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.imfile.close() - - def _get_sblk(self): - """ - Read and check the superblock. - """ - fmt = "<5I6H8Q" - size = calcsize(fmt) - - self.imfile.seek(0) - return SBlk(*unpack(fmt, self.imfile.read(size))) - - @staticmethod - def _fragment_block_entry(chunk): - """ - Interpret as fragment block entry. - """ - fmt = " 0: - entry = self._fragment_block_entry(data) - frag_entries.append(entry) - data = data[16:] # each entry is 16 bytes - - return tuple(frag_entries) - - @staticmethod - def _get_size(csize): - """ - For fragment entries and fragment blocks, the information if the data is compressed or not is contained in the (1 << 24) bit of the size. - """ - compressed = not bool(csize & 0x1000000) - size = csize & 0xffffff - return compressed, size - - @staticmethod - def _metadata_header(chunk): - """ - Interprets as header of a metadata block - """ - header = unpack(" as inode header. - """ - fmt = "<4H2I" - chunk = chunk[:calcsize(fmt)] - return InodeHeader(*unpack(fmt, chunk)) - - def _basic_file_inode(self, chunk): - """ - Interprets as inode of a basic file. - """ - rest_fmt = "<4I" - rest_size = calcsize(rest_fmt) - rest_chunk, block_sizes_chunk = chunk[:rest_size], chunk[rest_size:] - start_block, fragment, offset, file_size = unpack(rest_fmt, rest_chunk) - - num_blocks = ceil(file_size / self.sblk.block_size) - if fragment != 0xffffffff: # There is a fragment. In that case block_sizes is only a list of the full blocks - num_blocks -= 1 - - bsizes_fmt = f"<{num_blocks}I" - bsizes_size = calcsize(bsizes_fmt) - block_sizes_chunk = block_sizes_chunk[:bsizes_size] - block_sizes = unpack(bsizes_fmt, block_sizes_chunk) - return BasicFileInode(start_block, fragment, offset, file_size, block_sizes) - - @staticmethod - def _directory_header(chunk): - """ - Interprets as a header in the directory table. - """ - fmt = "<3I" - chunk = chunk[:calcsize(fmt)] - return DirectoryHeader(*unpack(fmt, chunk)) - - @staticmethod - def _directory_entry(chunk): - """ - Interprets as an entry in the directory table. - """ - rest_fmt = ". - """ - data = self._get_metablock(self.sblk.directory_table_start) - bname = name.encode() - - while len(data) > 0: - header = self._directory_header(data) - data = data[12:] - - for _ in range(header.count + 1): - dentry = self._directory_entry(data) - if dentry.name == bname: - log(0, f"found {bname} in dentry {dentry} after dir header {header}") - return header, dentry - - data = data[len(dentry):] - - raise FileNotFoundError(f"{name} not found!") - - def _get_inode_from_pos(self, block_pos, pos_in_block): - """ - Get the inode for a basic file from the starting point of the block and the position in the block. - """ - data = self._get_metablock(block_pos) - data = data[pos_in_block:] - - header = self._inode_header(data) - data = data[16:] - - if header.inode_type == 2: # 2 is a basic file - return self._basic_file_inode(data) - - log(4, "inode types other than basic file are not implemented!") - return None - - def _get_inode(self, name): - """ - Get the inode for a basic file by its name. - """ - head_entry = self._get_dentry(name) - if not head_entry: - return head_entry - - dhead, dentry = head_entry - - block_pos = self.sblk.inode_table_start + dhead.start_block - pos_in_block = dentry.offset - - return self._get_inode_from_pos(block_pos, pos_in_block) - - def read_file_blocks(self, filename): - """ - Generator where each iteration returns a block of file as bytes. - """ - - inode = self._get_inode(filename) - - fragment = self._get_fragment(inode) - file_len = len(fragment) - - self.imfile.seek(inode.start_block) - curr_pos = self.imfile.tell() - - for bsize in inode.block_list: - compressed, size = self._get_size(bsize) - - if curr_pos != self.imfile.tell(): - log(3, "Pointer not at correct position. Moving.") - self.imfile.seek(curr_pos) - - block = self.imfile.read(size) - curr_pos = self.imfile.tell() - - if compressed: - block = self.zdecomp.decompress(block, size, self.sblk.block_size) - - file_len += len(block) - yield block - - if file_len != inode.file_size: - msg = f""" - Size of extracted file not correct. Something went wrong! - calculated file_len: {file_len}, given file_size: {inode.file_size} - """ - raise IOError(msg) - - yield fragment - - def extract_file(self, filename, target_dir): - """ - Extracts file to - """ - with open(os.path.join(target_dir, filename), "wb") as outfile: - for block in self.read_file_blocks(filename): - outfile.write(block) diff --git a/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm.py b/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm.py index 26e4d1d35..f20890d98 100644 --- a/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm.py +++ b/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm.py @@ -9,7 +9,6 @@ from ..kodiutils import browsesingle, localize, log, ok_dialog, open_file, progress_dialog, yesno_dialog from ..utils import diskspace, elfbinary64, 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 def select_best_chromeos_image(devices): @@ -156,11 +155,3 @@ def extract_widevine_chromeos(backup_path, image_path, image_version): return False return progress - - -def install_widevine_arm(backup_path): - """Wrapper for installing widevine either from Chrome browser image or Chrome OS image""" - if cdm_from_lacros(): - return install_widevine_arm_lacros(backup_path) - - return install_widevine_arm_chromeos(backup_path) diff --git a/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm_chromeos.py b/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm_chromeos.py index dd804d706..3d9c8cc64 100644 --- a/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm_chromeos.py +++ b/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm_chromeos.py @@ -125,7 +125,7 @@ def _find_file_naive(self, fname): return file_entry - def _find_file_properly(self, filename, path_to_file=("opt", "google", "chrome", "WidevineCdm", "_platform_specific", "cros_arm")): + def _find_file_properly(self, filename, path_to_file=("opt", "google", "chrome", "WidevineCdm", "_platform_specific", "cros_arm64")): """ Finds a file at a given path, or searches upwards if not found. diff --git a/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm_lacros.py b/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm_lacros.py deleted file mode 100644 index 1fd9eec2e..000000000 --- a/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/arm_lacros.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -# MIT License (see LICENSE.txt or https://opensource.org/licenses/MIT) -"""Implements ARM specific widevine functions for Lacros image""" - -import os -import json -from ctypes.util import find_library - -from .repo import cdm_from_repo -from .. import config -from ..kodiutils import exists, localize, log, mkdirs, open_file, progress_dialog -from ..utils import http_download, http_get, system_os, userspace64 -from ..unsquash import SquashFs - - -def cdm_from_lacros(): - """Whether the Widevine CDM can/should be extracted from a lacros image""" - return not cdm_from_repo() and bool(find_library("zstd")) # The lacros images are compressed with zstd - - -def latest_lacros(): - """Finds the version of the latest stable lacros image""" - latest = json.loads(http_get(config.LACROS_LATEST))[0]["version"] - log(0, f"latest lacros image version is {latest}") - return latest - - -def extract_widevine_lacros(dl_path, backup_path, img_version): - """Extract Widevine from the given Lacros image""" - progress = progress_dialog() - progress.create(heading=localize(30043), message=localize(30044)) # Extracting Widevine CDM, prepping image - - fnames = (config.WIDEVINE_CDM_FILENAME[system_os()], config.WIDEVINE_MANIFEST_FILE, "LICENSE") # Here it's not LICENSE.txt, as defined in the config.py - bpath = os.path.join(backup_path, img_version) - if not exists(bpath): - mkdirs(bpath) - - try: - with SquashFs(dl_path) as sfs: - for num, fname in enumerate(fnames): - sfs.extract_file(fname, bpath) - progress.update(int(90 / len(fnames) * (num + 1)), localize(30048)) # Extracting from image - - except (IOError, FileNotFoundError) as err: - log(4, "SquashFs raised an error") - log(4, err) - return False - - with open_file(os.path.join(bpath, config.WIDEVINE_MANIFEST_FILE), "r") as manifest_file: - manifest_json = json.load(manifest_file) - - manifest_json.update({"img_version": img_version}) - - with open_file(os.path.join(bpath, config.WIDEVINE_MANIFEST_FILE), "w") as manifest_file: - json.dump(manifest_json, manifest_file, indent=2) - - log(0, f"Successfully extracted all files from lacros image {os.path.basename(dl_path)}") - return progress - - -def install_widevine_arm_lacros(backup_path, img_version=None): - """Installs Widevine CDM extracted from a Chrome browser SquashFS image on ARM-based architectures.""" - - if not img_version: - img_version = latest_lacros() - - url = config.LACROS_DOWNLOAD_URL.format(version=img_version, arch=("arm64" if userspace64() else "arm")) - - dl_path = http_download(url, message=localize(30072)) - - if dl_path: - progress = extract_widevine_lacros(dl_path, backup_path, img_version) - if progress: - return (progress, img_version) - - return False diff --git a/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/widevine.py b/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/widevine.py index 6cdff7100..0d06b6dbd 100644 --- a/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/widevine.py +++ b/script.module.inputstreamhelper/lib/inputstreamhelper/widevine/widevine.py @@ -12,7 +12,6 @@ from ..unicodes import compat_path, to_unicode from ..utils import (arch, cmd_exists, hardlink, http_download, parse_version, remove_tree, run_cmd, system_os) -from .arm_lacros import cdm_from_lacros, latest_lacros from .repo import cdm_from_repo, latest_widevine_available_from_repo @@ -67,7 +66,7 @@ def widevine_config_path(): iacdm = ia_cdm_path() if iacdm is None: return None - if cdm_from_repo() or cdm_from_lacros(): + if cdm_from_repo(): return os.path.join(iacdm, config.WIDEVINE_CONFIG_NAME) return os.path.join(iacdm, 'config.json') @@ -126,6 +125,9 @@ def missing_widevine_libs(): if system_os() != 'Linux': # this should only be needed for linux return None + if arch() in {'arm', 'arm64'}: # ldd will fail with missing GLIBC_ABI_DT_RELR error and is useless + return None + if cmd_exists('ldd'): widevinecdm = widevinecdm_path() if not os.access(widevinecdm, os.X_OK): @@ -157,13 +159,10 @@ def missing_widevine_libs(): def latest_widevine_version(): - """Returns the latest available version of Widevine CDM/Chrome OS/Lacros Image.""" + """Returns the latest available version of Widevine CDM/Chrome OS""" if cdm_from_repo(): return latest_widevine_available_from_repo(config.WIDEVINE_OS_MAP[system_os()], config.WIDEVINE_ARCH_MAP_REPO[arch()]).get('version') - if cdm_from_lacros(): - return latest_lacros() - from .arm import chromeos_config, select_best_chromeos_image devices = chromeos_config() arm_device = select_best_chromeos_image(devices) diff --git a/script.module.inputstreamhelper/resources/language/resource.language.en_gb/strings.po b/script.module.inputstreamhelper/resources/language/resource.language.en_gb/strings.po index c35043475..569945d8d 100644 --- a/script.module.inputstreamhelper/resources/language/resource.language.en_gb/strings.po +++ b/script.module.inputstreamhelper/resources/language/resource.language.en_gb/strings.po @@ -319,10 +319,6 @@ msgctxt "#30824" msgid "It is installed at [B]{path}[/B]" msgstr "" -msgctxt "#30825" -msgid "It was extracted from {image} image version [B]{version}[/B]" -msgstr "" - msgctxt "#30826" msgid "[B]Widevine CDM[/B] is [B]built into webOS[/B]" msgstr ""