diff --git a/README.md b/README.md index 1420b88e..7cb28666 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ Certain values can be set via environment variables, using the `-e` parameter on * __YTP_DB_FILE__: The path to the SQLite database file. Defaults to `{config_path}/ytptube.db`. * __YTP_MANUAL_ARCHIVE__: The path to the manual archive file. Defaults to `{config_path}/manual_archive.log`. * __YTP_UI_UPDATE_TITLE__: Whether to update the title of the page with the current stats. Defaults to `true`. +* __YTP_PIP_PACKAGES__: a space separated list of pip packages to install. Defaults to `""`, you can also use `{config_path}/pip.txt` to install the packages. +* __YTP_PIP_IGNORE_UPDATES__: Do not update the custom pip packages. Defaults to `false`. ## Running behind a reverse proxy diff --git a/app/library/PackageInstaller.py b/app/library/PackageInstaller.py new file mode 100644 index 00000000..177d473d --- /dev/null +++ b/app/library/PackageInstaller.py @@ -0,0 +1,60 @@ +import logging +import os +import subprocess +import sys +import importlib +from library.config import Config + +LOG = logging.getLogger("package_installer") + + +class PackageInstaller: + """ + This class is responsible for installing and upgrading pip packages. + """ + + def __init__(self, config: Config): + self.config: Config = config + + def from_env(self): + return self.config.pip_packages.split() if self.config.pip_packages else [] + + def from_file(self, file_path): + if not os.path.exists(file_path): + return [] + + if os.access(file_path, os.R_OK): + with open(file_path, "r") as f: + return [pkg.strip() for pkg in f.readlines() if pkg.strip()] + else: + LOG.error(f"Could not read pip packages from '{file_path}'.") + return [] + + def action(self, pkg: str): + try: + importlib.import_module(pkg) + if self.config.pip_ignore_updates: + LOG.info(f"'{pkg}' is already installed. Skipping upgrades. as requested.") + return + LOG.info(f"'{pkg}' is already installed. Checking for upgrades...") + subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", pkg], check=True) + except ImportError: + LOG.info(f"'{pkg}' is not installed. Installing...") + subprocess.run([sys.executable, "-m", "pip", "install", pkg], check=True) + + def check(self): + """ + Checks for user supplied pip packages and installs them if they are not already installed. + """ + pip_file = os.path.join(self.config.config_path, "pip.txt") + packages = list(set(self.from_env() + self.from_file(pip_file))) + + if not packages: + return + + for package in packages: + try: + self.action(package) + except Exception as e: + LOG.error(f"Failed to install or upgrade package '{package}'. Error message: {str(e)}") + LOG.exception(e) diff --git a/app/library/config.py b/app/library/config.py index ce4c1c36..db6ae847 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -56,6 +56,8 @@ class Config: db_file: str = "{config_path}/ytptube.db" manual_archive: str = "{config_path}/archive.manual.log" ui_update_title: bool = True + pip_packages: str = "" + pip_ignore_updates: bool = False # immutable config vars. version: str = APP_VERSION @@ -146,6 +148,7 @@ class Config: "remove_files", "ignore_ui", "ui_update_title", + "pip_ignore_updates", ) _frontend_vars: tuple = ( diff --git a/app/main.py b/app/main.py index 1511b0e9..5325f3f7 100644 --- a/app/main.py +++ b/app/main.py @@ -19,6 +19,7 @@ from library.encoder import Encoder from library.HttpAPI import HttpAPI, LOG as http_logger from library.HttpSocket import HttpSocket from library.Webhooks import Webhooks +from library.PackageInstaller import PackageInstaller LOG = logging.getLogger("app") MIME = magic.Magic(mime=True) @@ -37,6 +38,13 @@ class Main: self.encoder = Encoder() self.checkFolders() + + try: + PackageInstaller(self.config).check() + except Exception as e: + LOG.error(f"Failed to check for packages. Error message '{str(e)}'.") + LOG.exception(e) + caribou.upgrade(self.config.db_file, os.path.join(self.rootPath, "migrations")) connection = sqlite3.connect(database=self.config.db_file, isolation_level=None)