diff --git a/app/library/PackageInstaller.py b/app/library/PackageInstaller.py index b736123c..c43ef67f 100644 --- a/app/library/PackageInstaller.py +++ b/app/library/PackageInstaller.py @@ -10,16 +10,6 @@ import httpx LOG: logging.Logger = logging.getLogger("package_installer") -if base_dir := os.environ.get("YTP_CONFIG_PATH"): - base_dir = Path(base_dir) - user_site: Path = base_dir / f"python{sys.version_info.major}.{sys.version_info.minor}-packages" - - if not user_site.exists(): - user_site.mkdir(parents=True, exist_ok=True) - - if user_site.is_dir() and str(user_site) not in sys.path: - sys.path.insert(0, str(user_site)) - def parse_version(v: str) -> tuple[int, ...]: return tuple(int("".join(filter(str.isdigit, part)) or "0") for part in v.strip().split(".")) @@ -49,16 +39,23 @@ class Packages: class PackageInstaller: user_site: Path | None = None - def __init__(self): - if base_dir := os.environ.get("YTP_CONFIG_PATH"): + def __init__(self, pkg_path: Path | None = None): + if pkg_path: + self.user_site = pkg_path + + if not self.user_site and (base_dir := os.environ.get("YTP_CONFIG_PATH")): base_dir = Path(base_dir) self.user_site = base_dir / f"python{sys.version_info.major}.{sys.version_info.minor}-packages" self.user_site.mkdir(parents=True, exist_ok=True) - if str(self.user_site) not in sys.path: - sys.path.insert(0, str(self.user_site)) + if not self.user_site: + LOG.error("No valid package path provided or found in environment. Aborting.") + return - def action(self, pkg: str, upgrade: bool = False): + if str(self.user_site) not in sys.path: + sys.path.insert(0, str(self.user_site)) + + def action(self, pkg: str, upgrade: bool = False) -> None: version: str | None = None if "==" in pkg: pkg, version = pkg.split("==", 1) @@ -89,7 +86,7 @@ class PackageInstaller: return None def _get_latest_version(self, pkg: str) -> str | None: - url = f"https://pypi.org/pypi/{pkg}/json" + url: str = f"https://pypi.org/pypi/{pkg}/json" try: with httpx.Client(timeout=5.0) as client: resp = client.get(url) @@ -100,7 +97,7 @@ class PackageInstaller: LOG.warning(f"Error while querying PyPI for '{pkg}': {e}") return None - def _install_pkg(self, pkg: str, version: str | None = None): + def _install_pkg(self, pkg: str, version: str | None = None) -> bool: cmd: list[str] = [ sys.executable, "-m", @@ -130,6 +127,8 @@ class PackageInstaller: creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0, ) self.out(out=proc.stdout, err=proc.stderr) + + return 0 == proc.returncode except subprocess.CalledProcessError as e: LOG.error(f"Failed to install '{pkg}' (exit {e.returncode}). {e!s}") self.out(out=e.stdout, err=e.stderr) @@ -163,8 +162,8 @@ class PackageInstaller: return True # Handle yt-dlp version format differences - current_parts = current.split(".") - target_parts = target.split(".") + current_parts: list[str] = current.split(".") + target_parts: list[str] = target.split(".") if len(current_parts) == len(target_parts): # Normalize parts by zero-padding single digits diff --git a/app/upgrader.py b/app/upgrader.py index abd98dbd..30a36a71 100644 --- a/app/upgrader.py +++ b/app/upgrader.py @@ -10,21 +10,10 @@ import logging import os from pathlib import Path -from dotenv import load_dotenv from library.PackageInstaller import PackageInstaller, Packages LOG: logging.Logger = logging.getLogger("upgrader") -if base_dir := os.environ.get("YTP_CONFIG_PATH"): - base_dir = Path(base_dir) - user_site: Path = base_dir / f"python{sys.version_info.major}.{sys.version_info.minor}-packages" - - if not user_site.exists(): - user_site.mkdir(parents=True, exist_ok=True) - - if user_site.is_dir() and str(user_site) not in sys.path: - sys.path.insert(0, str(user_site)) - class Upgrader: def __init__(self): @@ -34,15 +23,52 @@ class Upgrader: else: os.environ.update({"YTP_CONFIG_PATH": str(config_path)}) - if config_path.exists(): - envFile: Path = config_path / ".env" - if envFile.exists(): - LOG.debug(f"loading environment variables from '{envFile}'.") - load_dotenv(str(envFile)) - else: + if not config_path.exists(): LOG.error(f"config path '{config_path}' doesn't exists.") + return - pkg_installer = PackageInstaller() + envFile: Path = config_path / ".env" + if envFile.exists(): + from dotenv import load_dotenv + + LOG.debug(f"loading environment variables from '{envFile}'.") + load_dotenv(str(envFile)) + + user_site: Path = config_path / f"python{sys.version_info.major}.{sys.version_info.minor}-packages" + + if not user_site.exists(): + user_site.mkdir(parents=True, exist_ok=True) + + if user_site.is_dir(): + if str(user_site) not in sys.path: + sys.path.insert(0, str(user_site)) + + from app.library.version import APP_VERSION + + user_site_ver: Path = user_site / ".version" + if not user_site_ver.exists() or user_site_ver.read_text().strip() != APP_VERSION: + self.clean_up(user_site, user_site_ver, APP_VERSION) + + self.check(config_path, user_site) + + def clean_up(self, user_site: Path, user_site_ver: Path, app_version: str) -> None: + LOG.info(f"Cleaning up user site packages at '{user_site}' for app version '{app_version}'.") + from app.library.Utils import delete_dir + + if not delete_dir(user_site): + LOG.error(f"Failed to clean up user site packages at '{user_site}'.") + return + + try: + user_site.mkdir(parents=True, exist_ok=True) + user_site_ver.write_text(app_version) + LOG.info(f"User site packages cleaned up and ready for app version '{app_version}'.") + except Exception as e: + LOG.exception(e) + LOG.error(f"Failed to recreate user site packages at '{user_site}'. '{e!s}'.") + + def check(self, config_path: Path, user_site: Path) -> None: + pkg_installer = PackageInstaller(pkg_path=user_site) ytdlp_auto_update: bool = os.environ.get("YTP_YTDLP_AUTO_UPDATE", "true").strip().lower() == "true" ytdlp_version: str | None = os.environ.get("YTP_YTDLP_VERSION", "").strip() or None