UNPKG

homebridge-appletv-enhanced

Version:

Plugin that exposes the Apple TV to HomeKit with much richer features than the vanilla Apple TV implementation of HomeKit.

387 lines 18.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const utils_1 = require("./utils"); const PrefixLogger_1 = __importDefault(require("./PrefixLogger")); const axios_1 = __importDefault(require("axios")); const compare_versions_1 = require("compare-versions"); const os_1 = __importDefault(require("os")); let supportedPythonVersions = [ '3.9', '3.10', '3.11', '3.12', '3.13', ]; const MIN_OPENSSL_VERSION = '3.0.0'; const UID = os_1.default.userInfo().uid; const GID = os_1.default.userInfo().gid; class PythonChecker { customPythonExecutable; log; pluginDirPath; pythonExecutable = 'python3'; pythonRequirementsProfile = 'default'; venvAtvremoteExecutable; venvAtvscriptExecutable; venvConfigPath; venvPath; venvPipExecutable; venvPythonExecutable; constructor(logger, storagePath, customPythonExecutable) { this.log = new PrefixLogger_1.default(logger, 'Python check'); this.customPythonExecutable = customPythonExecutable; this.pluginDirPath = path_1.default.join(storagePath, 'appletv-enhanced'); this.venvPath = path_1.default.join(this.pluginDirPath, '.venv'); this.venvPythonExecutable = path_1.default.join(this.venvPath, 'bin', 'python3'); this.venvPipExecutable = path_1.default.join(this.venvPath, 'bin', 'pip3'); this.venvConfigPath = path_1.default.join(this.venvPath, 'pyvenv.cfg'); this.venvAtvremoteExecutable = path_1.default.join(this.venvPath, 'bin', 'atvremote'); this.venvAtvscriptExecutable = path_1.default.join(this.venvPath, 'bin', 'atvscript'); } async allInOne(forceVenvRecreate = false) { this.log.info('Starting python check.'); this.pythonExecutable = this.getPythonExecutable('python3', this.customPythonExecutable); this.log.info(`Using "${this.pythonExecutable}" as the python executable.`); this.ensurePluginDir(); await this.openSSL(); await this.ensurePythonVersion(); await this.ensureVenvCreated(forceVenvRecreate); await this.ensureVenvUsesCorrectPython(); await this.ensureVenvPipUpToDate(); await this.ensureVenvRequirementsSatisfied(); if (this.pythonRequirementsProfile === 'default') { await this.updateApiPy(); } this.log.success('Finished'); } async areRequirementsSatisfied() { const requirementsPath = path_1.default.join(__dirname, '..', 'python_requirements', this.pythonRequirementsProfile, 'requirements.txt'); const [freezeStdout] = await (0, utils_1.runCommand)(this.log, this.venvPipExecutable, ['freeze'], undefined, true); const freeze = this.freezeStringToObject(freezeStdout); const requirements = this.freezeStringToObject(fs_1.default.readFileSync(requirementsPath).toString()); for (const pkg in requirements) { if (freeze[pkg] !== requirements[pkg]) { return false; } } return true; } async createVenv() { const [stdout] = await (0, utils_1.runCommand)(this.log, this.pythonExecutable, ['-m', 'venv', this.venvPath, '--clear'], undefined, true); if (stdout.includes('not created successfully') || !this.isVenvCreated()) { while (true) { this.log.error('virtualenv python module is not installed. If you have installed homebridge via the apt package manager, \ update the homebridge apt package to 1.1.4 or above (this applies for installations based on the Raspberry Pi OS image as well). When \ using the official docker image, update the image to version 2023-11-28 or above. Otherwise install the python virtualenv module \ manually.'); await (0, utils_1.delay)(300000); } } else if (stdout.trim() !== '') { this.log.warn(stdout); } this.log.success('Virtual python environment (re)created'); } ensurePluginDir() { if (!fs_1.default.existsSync(this.pluginDirPath)) { this.log.info('creating plugin dir ...'); fs_1.default.mkdirSync(this.pluginDirPath); this.log.success('plugin dir created'); } else { this.log.info('plugin dir exists.'); } } async ensurePythonVersion() { const version = await this.getSystemPythonVersion(); if (supportedPythonVersions.findIndex((e) => version.startsWith(e)) === -1) { while (true) { this.log.error(`Python ${version} is installed. However, only Python \ ${supportedPythonVersions[0]} to ${supportedPythonVersions[supportedPythonVersions.length - 1]} is supported.`); await (0, utils_1.delay)(300000); } } else { this.log.info(`Python ${version} is installed and supported by the plugin.`); } } async ensureVenvCreated(forceVenvRecreate) { if (forceVenvRecreate) { this.log.warn('Forcing the python virtual environment to be recreated ...'); await this.createVenv(); } else if (this.isVenvCreated() === false) { this.log.info('Virtual python environment is not present. Creating now ...'); await this.createVenv(); } else if (this.isVenvExecutable() === false) { while (true) { this.log.error(`The current user ${UID}:${GID} does not have the permissions to execute the virtual python environment. \ Make sure the user has the permissions to execute the above mentioned files. \`chmod +x ./appletv-enhanced/.venv/bin/*\` should do the \ trick. Restart the plugin after fixing the permissions.`); await (0, utils_1.delay)(300000); } } else { this.log.info('Virtual environment already exists.'); } } async ensureVenvPipUpToDate() { const venvPipVersion = await this.getVenvPipVersion(); this.log.info(`Venv pip version: ${venvPipVersion}`); this.log.info('Checking if there is an update for venv pip ...'); if (venvPipVersion === await this.getMostRecentPipVersion()) { this.log.info('Venv pip is up to date'); } else { this.log.warn('Venv pip is outdated. Updating now ...'); const success = await this.updatePip(); if (success === true) { this.log.success('Venv pip successfully updated.'); } else { this.log.warn('Failed to update venv pip. Continuing anyhow ...'); } } } async ensureVenvRequirementsSatisfied() { if (await this.areRequirementsSatisfied()) { this.log.info('Python requirements are satisfied.'); } else { this.log.warn('Python requirements are not satisfied. Installing them now ...'); const success = await this.installRequirements(); if (success === true) { this.log.success('Python requirements successfully installed.'); } else { while (true) { this.log.error('There was an error installing the python dependencies. Cannot proceed!'); await (0, utils_1.delay)(300000); } } } if (this.isPyatvExecutable() === false) { while (true) { this.log.error(`The current user ${UID}:${GID} does not have the permissions to execute the PyATV scripts. \ Make sure the user has the permissions to execute the above mentioned files. \`chmod +x ./appletv-enhanced/.venv/bin/*\` should do the \ trick. Restart the plugin after fixing the permissions.`); await (0, utils_1.delay)(300000); } } } async ensureVenvUsesCorrectPython() { const systemPythonHome = await this.getPythonHome(this.pythonExecutable); this.log.debug(`System python home: ${systemPythonHome}`); const venvPythonHome = await this.getPythonHome(this.venvPythonExecutable); this.log.debug(`Venv python home: ${venvPythonHome}`); if (venvPythonHome !== systemPythonHome) { this.log.warn('The virtual environment does not use the configured python environment. Recreating virtual environment ...'); await this.createVenv(); return; } const systemPythonVersion = await this.getSystemPythonVersion(); this.log.debug(`System python version: ${systemPythonVersion}`); const venvPythonVersion = this.getVenvPythonVersion(); this.log.debug(`Venv python version: ${venvPythonVersion}`); if (systemPythonVersion !== venvPythonVersion) { this.log.warn('The virtual environment does not use the configured python environment. Recreating virtual environment ...'); await this.createVenv(); return; } this.log.info('Virtual environment is using the configured python environment. Continuing ...'); } freezeStringToObject(value) { const lines = value.trim().split('\n'); const packages = {}; for (const line of lines) { const [pkg, version] = line.split('=='); packages[pkg.replaceAll('_', '-')] = version; } return packages; } async getMostRecentPipVersion() { try { const response = await axios_1.default.get('https://pypi.org/pypi/pip/json'); return response.data.info.version; } catch (e) { this.log.error(e); return 'error'; } } getPythonExecutable(defaultsTo, customPythonExecutable) { if (customPythonExecutable === undefined) { this.log.debug('Using the systems default python installation since there is no custom python installation specified.'); return defaultsTo; } const pythonCandidate = (0, utils_1.normalizePath)(customPythonExecutable); if (pythonCandidate === undefined) { this.log.warn(`Could not normalize the python executable path ${pythonCandidate}. Falling back to the systems default python \ installation.`); return defaultsTo; } if (fs_1.default.existsSync(pythonCandidate)) { this.log.debug(`The specified python installation "${pythonCandidate}" does exist.`); try { fs_1.default.accessSync(pythonCandidate, fs_1.default.constants.X_OK); this.log.debug(`The current user ${UID}:${GID} has the permissions to execute "${pythonCandidate}".`); } catch { this.log.warn(`The current user ${UID}:${GID} does not have the permissions to execute "${pythonCandidate}". Falling back \ to the systems default python installation.`); return defaultsTo; } this.log.debug(`Using the specified python installation "${pythonCandidate}".`); return pythonCandidate; } this.log.warn(`The python executable "${pythonCandidate}" set in the configuration does not exist. Falling back to the \ systems default python installation.`); return defaultsTo; } async getPythonHome(executable) { const [venvPythonHome] = await (0, utils_1.runCommand)(this.log, executable, [path_1.default.join(__dirname, 'determinePythonHome.py')], undefined, true); return venvPythonHome.trim(); } async getSystemPythonVersion() { const [version] = await (0, utils_1.runCommand)(this.log, this.pythonExecutable, ['--version'], undefined, true); return version.trim().replace('Python ', ''); } async getVenvPipVersion() { const [version] = await (0, utils_1.runCommand)(this.log, this.venvPipExecutable, ['--version'], undefined, true); return version.trim().replace('pip ', '').split(' ')[0]; } getVenvPythonVersion() { const pyvenvcfgContent = fs_1.default.readFileSync(this.venvConfigPath, 'utf-8'); const lines = pyvenvcfgContent.split('\n'); const versionLine = lines.find((e) => e.startsWith('version')); if (versionLine === undefined) { return '?'; } return versionLine.replace('version', '').replace('=', '').trim(); } async installRequirements() { const requirementsPath = path_1.default.join(__dirname, '..', 'python_requirements', this.pythonRequirementsProfile, 'requirements.txt'); return (await (0, utils_1.runCommand)(this.log, this.venvPipExecutable, ['install', '-r', requirementsPath])).at(2) === 0; } isPyatvExecutable() { let success = true; for (const f of [ this.venvAtvremoteExecutable, this.venvAtvscriptExecutable, ]) { try { fs_1.default.accessSync(f, fs_1.default.constants.X_OK); } catch { this.log.warn(`The current user ${UID}:${GID} does not have the permissions to execute "${f}".`); success = false; } } return success; } isVenvCreated() { let success = true; for (const f of [ this.venvConfigPath, this.venvPythonExecutable, this.venvPipExecutable, ]) { if (fs_1.default.existsSync(f) === false) { this.log.debug(`${f} does not exist --> venv is not present`); success = false; } } return success; } isVenvExecutable() { let success = true; for (const f of [ this.venvPythonExecutable, this.venvPipExecutable, ]) { try { fs_1.default.accessSync(f, fs_1.default.constants.X_OK); } catch { this.log.warn(`The current user ${UID}:${GID} does not have the permissions to execute "${f}".`); success = false; } } return success; } async openSSL() { const [openSSLVersionString] = await (0, utils_1.runCommand)(this.log, 'openssl', ['version'], undefined, true); const r = openSSLVersionString.match(/\d+\.\d+\.\d+/); if (r !== null && (0, compare_versions_1.compareVersions)(MIN_OPENSSL_VERSION, r[0]) !== 1) { this.log.info(`OpenSSL ${r[0]} is installed and compatible.`); return; } if (r === null) { this.log.warn('Could not verify that the correct OpenSSL version is installed. Falling back to openssl legacy mode. Be aware \ that Python 3.12 or later is not compatible with openssl legacy mode.'); } else { this.log.warn(`You are using OpenSSL ${r[0]}. However, OpenSSL ${MIN_OPENSSL_VERSION} or later is required for the most recent \ AppleTV enhanced version. Falling back to openssl legacy mode. Be aware that Python 3.12 or later is not compatible with openssl legacy \ mode.`); } this.pythonRequirementsProfile = 'openssl_legacy'; supportedPythonVersions = supportedPythonVersions.filter((e) => e !== '3.12' && e !== '3.13'); } async updateApiPy() { this.log.info('Downloading a temporary fix for the TvOS 18.4 connection issue. Refer to this GitHub issue for more information: \ https://github.com/maxileith/homebridge-appletv-enhanced/issues/953'); // download Api.py let response = undefined; try { response = await axios_1.default.get('https://raw.githubusercontent.com/postlund/pyatv/3fe8e36caf1977d2c7dced4767ada12c95a3e7c3/pyatv/\ protocols/companion/api.py', { timeout: 1000 }); this.log.success('Successfully downloaded the fix'); } catch (e) { if (e instanceof Error) { this.log.warn(`Failed to download the fix, continuing without downloading the fix. (${e.name}: ${e.message})`); return; } else { throw e; } } // write Api.pyconst requirementsPath: string = this.log.info('Installing the fix'); const libPath = path_1.default.join(this.venvPath, 'lib'); const libPath64 = path_1.default.join(this.venvPath, 'lib'); const pythonDir = fs_1.default.readdirSync(libPath, { withFileTypes: true }).find((e) => e.isDirectory()); if (pythonDir === undefined) { this.log.warn('Failed to determine the location where the fix needs to be installed. Continuing without installing the fix.'); return; } const apiPyPath = path_1.default.join(libPath, pythonDir.name, 'site-packages', 'pyatv', 'protocols', 'companion', 'api.py'); const apiPyPath64 = path_1.default.join(libPath64, pythonDir.name, 'site-packages', 'pyatv', 'protocols', 'companion', 'api.py'); const apiPyPathExists = fs_1.default.existsSync(apiPyPath); const apiPyPath64Exists = fs_1.default.existsSync(apiPyPath64); if (apiPyPathExists) { fs_1.default.writeFileSync(apiPyPath, response.data, { encoding: 'utf8', flag: 'w' }); } if (apiPyPath64Exists) { fs_1.default.writeFileSync(apiPyPath64, response.data, { encoding: 'utf8', flag: 'w' }); } if (apiPyPathExists === false && apiPyPath64Exists === false) { this.log.warn('Failed to determine the location where the fix needs to be installed. Continuing without installing the fix.'); } else { this.log.success('Successfully installed the fix'); } } async updatePip() { return (await (0, utils_1.runCommand)(this.log, this.venvPipExecutable, ['install', '--upgrade', 'pip'])).at(2) === 0; } } exports.default = PythonChecker; //# sourceMappingURL=PythonChecker.js.map