From cfd791d415c280cf970d2c42253ddc521f8b5cba Mon Sep 17 00:00:00 2001 From: Nose Date: Wed, 31 Dec 2025 22:05:25 -0800 Subject: [PATCH] nh --- .gitignore | 5 +- SYS/optional_deps.py | 115 ++++++---- cmdlet/_shared.py | 7 - cmdlet/add_file.py | 8 +- pyproject.toml | 4 +- readme.md | 7 +- scripts/bootstrap.ps1 | 31 --- scripts/bootstrap.py | 214 ++++++++++++------ scripts/hydrusnetwork.py | 15 +- .../requirements-dev.txt | 0 requirements.txt => scripts/requirements.txt | 13 +- scripts/run_client.py | 4 +- 12 files changed, 248 insertions(+), 175 deletions(-) rename requirements-dev.txt => scripts/requirements-dev.txt (100%) rename requirements.txt => scripts/requirements.txt (71%) diff --git a/.gitignore b/.gitignore index 39bcd72..d77c823 100644 --- a/.gitignore +++ b/.gitignore @@ -232,6 +232,9 @@ hydrusnetwork .style.yapf .yapfignore tests/ - +scripts/mm.ps1 +scripts/mm +.style.yapf +.yapfignore diff --git a/SYS/optional_deps.py b/SYS/optional_deps.py index f53657e..1ad0aa0 100644 --- a/SYS/optional_deps.py +++ b/SYS/optional_deps.py @@ -35,20 +35,45 @@ def _try_import(module: str) -> bool: return False +_FLORENCEVISION_DEPENDENCIES: List[Tuple[str, str]] = [ + ("transformers", "transformers>=4.45.0"), + ("torch", "torch>=2.4.0"), + ("PIL", "Pillow>=10.0.0"), + ("einops", "einops>=0.8.0"), + ("timm", "timm>=1.0.0"), +] + +_PROVIDER_DEPENDENCIES: Dict[str, List[Tuple[str, str]]] = { + "telegram": [("telethon", "telethon>=1.36.0")], + "soulseek": [("aioslsk", "aioslsk>=1.6.0")], +} + + def florencevision_missing_modules() -> List[str]: - missing: List[str] = [] - # pillow is already in requirements, but keep the check for robustness. - if not _try_import("transformers"): - missing.append("transformers") - if not _try_import("torch"): - missing.append("torch") - if not _try_import("PIL"): - missing.append("pillow") - # Florence-2 remote code frequently requires these extras. - if not _try_import("einops"): - missing.append("einops") - if not _try_import("timm"): - missing.append("timm") + return [ + requirement + for import_name, requirement in _FLORENCEVISION_DEPENDENCIES + if not _try_import(import_name) + ] + + +def _provider_missing_modules(config: Dict[str, Any]) -> Dict[str, List[str]]: + missing: Dict[str, List[str]] = {} + provider_cfg = (config or {}).get("provider") + if not isinstance(provider_cfg, dict): + return missing + + for provider_name, requirements in _PROVIDER_DEPENDENCIES.items(): + block = provider_cfg.get(provider_name) + if not isinstance(block, dict) or not block: + continue + missing_for_provider = [ + requirement + for import_name, requirement in requirements + if not _try_import(import_name) + ] + if missing_for_provider: + missing[provider_name] = missing_for_provider return missing @@ -73,47 +98,51 @@ def _pip_install(requirements: List[str]) -> Tuple[bool, str]: return False, str(exc) +def _install_requirements(label: str, requirements: List[str]) -> None: + if not requirements: + return + + names = ", ".join(requirements) + status_text = f"Installing {label} dependencies: {names}" + try: + with stdout_console().status(status_text, spinner="dots"): + ok, detail = _pip_install(requirements) + except Exception: + log(f"[startup] {label} dependencies missing ({names}). Attempting auto-install...") + ok, detail = _pip_install(requirements) + + if ok: + log(f"[startup] {label} dependency install OK") + else: + log(f"[startup] {label} dependency auto-install failed. {detail}") + + def maybe_auto_install_configured_tools(config: Dict[str, Any]) -> None: - """Best-effort dependency auto-installer for configured tools. + """Best-effort dependency auto-installer for configured tools and providers. This is intentionally conservative: - - Only acts when a tool block is enabled. + - Only acts when a configuration block is present/enabled. - Skips under pytest. - Current supported tool(s): florencevision + Current supported features: FlorenceVision tool, Telegram provider, Soulseek provider """ if _is_pytest(): return tool_cfg = (config or {}).get("tool") - if not isinstance(tool_cfg, dict): - return + if isinstance(tool_cfg, dict): + fv = tool_cfg.get("florencevision") + if isinstance(fv, dict) and _as_bool(fv.get("enabled"), False): + auto_install = _as_bool(fv.get("auto_install"), True) + if auto_install: + missing = florencevision_missing_modules() + if missing: + _install_requirements("FlorenceVision", missing) - fv = tool_cfg.get("florencevision") - if isinstance(fv, dict) and _as_bool(fv.get("enabled"), False): - auto_install = _as_bool(fv.get("auto_install"), True) - if not auto_install: - return - - missing = florencevision_missing_modules() - if not missing: - return - - names = ", ".join(missing) - try: - with stdout_console().status( - f"Installing FlorenceVision dependencies: {names}", - spinner="dots", - ): - ok, detail = _pip_install(missing) - except Exception: - log(f"[startup] FlorenceVision dependencies missing ({names}). Attempting auto-install...") - ok, detail = _pip_install(missing) - - if ok: - log("[startup] FlorenceVision dependency install OK") - else: - log(f"[startup] FlorenceVision dependency auto-install failed. {detail}") + provider_missing = _provider_missing_modules(config) + for provider_name, requirements in provider_missing.items(): + label = f"{provider_name.title()} provider" + _install_requirements(label, requirements) __all__ = ["maybe_auto_install_configured_tools", "florencevision_missing_modules"] diff --git a/cmdlet/_shared.py b/cmdlet/_shared.py index a3d1aab..72f75f1 100644 --- a/cmdlet/_shared.py +++ b/cmdlet/_shared.py @@ -2567,13 +2567,6 @@ def resolve_tidal_manifest_path(item: Any) -> Optional[str]: metadata["_tidal_manifest_url"] = selected_url except Exception: pass - try: - log( - f"[hifi] Resolved JSON manifest for track {metadata.get('trackId') or metadata.get('id')} to {selected_url}", - file=sys.stderr, - ) - except Exception: - pass return selected_url try: metadata["_tidal_manifest_error"] = "JSON manifest contained no urls" diff --git a/cmdlet/add_file.py b/cmdlet/add_file.py index 098c3f2..3815877 100644 --- a/cmdlet/add_file.py +++ b/cmdlet/add_file.py @@ -640,11 +640,13 @@ class Add_File(Cmdlet): or tidal_metadata.get("_tidal_manifest_url") ) if not manifest_source: + target_is_mpd = False if isinstance(media_path_or_url, Path): - manifest_source = media_path_or_url + target_is_mpd = str(media_path_or_url).lower().endswith(".mpd") elif isinstance(media_path_or_url, str): - if media_path_or_url.lower().endswith(".mpd"): - manifest_source = media_path_or_url + target_is_mpd = media_path_or_url.lower().endswith(".mpd") + if target_is_mpd: + manifest_source = media_path_or_url if manifest_source: downloaded, tmp_dir = self._download_manifest_with_ffmpeg(manifest_source) diff --git a/pyproject.toml b/pyproject.toml index 2b99eef..b5e7904 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,8 @@ dependencies = [ "yt-dlp[default]>=2023.11.0", "yt-dlp-ejs", # EJS challenge solver scripts for YouTube JavaScript challenges "requests>=2.31.0", + "charset-normalizer>=3.2.0", + "certifi>=2024.12.0", "httpx>=0.25.0", # Document and data handling @@ -57,7 +59,7 @@ dependencies = [ "lxml>=4.9.0", # Advanced searching and libraries - "aioslsk>=1.6.0", + # Optional Soulseek support installs aioslsk>=1.6.0 when [provider=soulseek] is configured. "imdbinfo>=0.1.10", # Encryption and security diff --git a/readme.md b/readme.md index ef11aa2..f5995f6 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,8 @@ Medios-Macina is a CLI media manager and toolkit focused on downloading, tagging - **Flexible syntax structure:** chain commands with `|` and select options from tables with `@N`. - **Multiple file stores:** *HYDRUSNETWORK, FOLDER* - **Provider plugin integration:** *YOUTUBE, OPENLIBRARY, INTERNETARCHIVE, SOULSEEK, LIBGEN, ALLDEBRID, TELEGRAM, BANDCAMP* -- **Module Mixing:** *[Playwright](https://github.com/microsoft/playwright), [yt-dlp](https://github.com/yt-dlp/yt-dlp), [aioslsk](https://github.com/JurgenR/aioslsk), [telethon](https://github.com/LonamiWebs/Telethon),[typer](https://github.com/fastapi/typer)* +- **Module Mixing:** *[Playwright](https://github.com/microsoft/playwright), [yt-dlp](https://github.com/yt-dlp/yt-dlp), [typer](https://github.com/fastapi/typer)* +- **Optional stacks:** Telethon (Telegram), aioslsk (Soulseek), and the FlorenceVision tooling install automatically when you configure the corresponding provider/tool blocks. - **MPV Manager:** Play audio, video, and even images in a custom designed MPV with trimming, screenshotting, and more built right in! ## installation ⚡ @@ -17,6 +18,10 @@ GIT CLONE https://code.glowers.club/goyimnose/Medios-Macina - When run interactively (a normal terminal), `bootstrap.py` will show a short menu to Install or Uninstall the project. For non-interactive runs use flags such as `--no-playwright`, `--uninstall`, or `-y` to assume yes for confirmations. + *Note:* If you run `--uninstall` while the local `.venv` is activated, the uninstaller will try to re-run the uninstall using a Python interpreter outside the venv so files can be removed safely (this is helpful on Windows). If no suitable interpreter can be found, deactivate the venv (`deactivate`) and re-run the uninstall. + + Optional providers/tools bring their own dependencies instead of shipping as part of the base install; just configure `[provider=telegram]`, `[provider=soulseek]`, or `[tool=florencevision]` and the CLI will install the required packages on startup. + 2. rename config.conf.remove to config.conf the store=folder path should be empty folder with no other files in it. ```ini diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 index a8df4b8..b52839e 100644 --- a/scripts/bootstrap.ps1 +++ b/scripts/bootstrap.ps1 @@ -526,41 +526,10 @@ try { $globalBin = Join-Path $env:USERPROFILE 'bin' New-Item -ItemType Directory -Path $globalBin -Force | Out-Null - $mmCmd = Join-Path $globalBin 'mm.cmd' $mmPs1 = Join-Path $globalBin 'mm.ps1' $repo = $repoRoot - $cmdText = @" -@echo off -set "REPO=__REPO__" -if exist "%REPO%\.venv\Scripts\mm.exe" "%REPO%\.venv\Scripts\mm.exe" %* -if defined MM_DEBUG ( - echo MM_DEBUG: REPO=%REPO% - if exist "%REPO%\.venv\Scripts\python.exe" ( - "%REPO%\.venv\Scripts\python.exe" -c "import sys,importlib,importlib.util; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);" - ) else ( - python -c "import sys,importlib,importlib.util; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);" - ) -) -if exist "%REPO%\.venv\Scripts\python.exe" ( - "%REPO%\.venv\Scripts\python.exe" -m medeia_macina.cli_entry %* - exit /b %ERRORLEVEL% -) -if exist "%REPO%\CLI.py" ( - python "%REPO%\CLI.py" %* - exit /b %ERRORLEVEL% -) -python -m medeia_macina.cli_entry %* -"@ - # Inject actual repo path safely (escape double-quotes if any) - $cmdText = $cmdText.Replace('__REPO__', $repo.Replace('"', '""')) - if (Test-Path $mmCmd) { - $bak = "$mmCmd.bak$(Get-Date -UFormat %s)" - Move-Item -Path $mmCmd -Destination $bak -Force - } - Set-Content -LiteralPath $mmCmd -Value $cmdText -Encoding UTF8 - # PowerShell shim: use single-quoted here-string so literal PowerShell variables # (like $args) are not expanded by this script when writing the file. $ps1Text = @' diff --git a/scripts/bootstrap.py b/scripts/bootstrap.py index 50a4964..19f9c71 100644 --- a/scripts/bootstrap.py +++ b/scripts/bootstrap.py @@ -3,7 +3,7 @@ Unified project bootstrap helper (Python-only). -This script installs Python dependencies from `requirements.txt` and then +This script installs Python dependencies from `scripts/requirements.txt` and then downloads Playwright browser binaries by running `python -m playwright install`. By default this script installs **Chromium** only to conserve space; pass `--browsers all` to install all supported engines (chromium, firefox, webkit). @@ -30,7 +30,7 @@ Usage: python ./scripts/bootstrap.py --playwright-only Optional flags: - --skip-deps Skip `pip install -r requirements.txt` step + --skip-deps Skip `pip install -r scripts/requirements.txt` step --no-playwright Skip running `python -m playwright install` (still installs deps) --playwright-only Install only Playwright browsers (installs playwright package if missing) --browsers Comma-separated list of Playwright browsers to install (default: chromium) @@ -223,7 +223,7 @@ def main() -> int: parser.add_argument( "--skip-deps", action="store_true", - help="Skip installing Python dependencies from requirements.txt", + help="Skip installing Python dependencies from scripts/requirements.txt", ) parser.add_argument( "--no-playwright", @@ -317,12 +317,102 @@ def main() -> int: return False def _do_uninstall() -> int: - """Attempt to remove the local venv and any shims written to the user's bin.""" + """Attempt to remove the local venv and any shims written to the user's bin. + + If this script is running using the Python inside the local `.venv`, we + attempt to re-run the uninstall using a Python interpreter outside the + venv (so files can be removed on Windows). If no suitable external + interpreter can be found, the user is asked to deactivate the venv and + re-run the uninstall. + """ vdir = repo_root / ".venv" if not vdir.exists(): if not args.quiet: print("No local .venv found; nothing to uninstall.") return 0 + + # If the current interpreter is the one inside the local venv, try to + # run the uninstall via a Python outside the venv so files (including + # the interpreter binary) can be removed on Windows. + try: + current_exe = Path(sys.executable).resolve() + in_venv = str(current_exe).lower().startswith(str(vdir.resolve()).lower()) + except Exception: + in_venv = False + + if in_venv: + if not args.quiet: + print(f"Detected local venv Python in use: {current_exe}") + + if not args.yes: + try: + resp = input("Uninstall will be attempted using a system Python outside the .venv. Continue? [Y/n]: ") + except EOFError: + print("Non-interactive environment; pass --uninstall --yes to uninstall without prompts.", file=sys.stderr) + return 2 + if resp.strip().lower() in ("n", "no"): + print("Uninstall aborted.") + return 1 + + def _find_external_python() -> list[str] | None: + """Return a command (list) for a Python interpreter outside the venv, or None.""" + try: + base = Path(sys.base_prefix) + candidates: list[Path | str] = [] + if platform.system().lower() == "windows": + candidates.append(base / "python.exe") + else: + candidates.extend([base / "bin" / "python3", base / "bin" / "python"]) + + for name in ("python3", "python"): + p = shutil.which(name) + if p: + candidates.append(Path(p)) + + # Special-case the Windows py launcher: ensure it resolves + # to a Python outside the venv before returning ['py','-3'] + if platform.system().lower() == "windows": + py_launcher = shutil.which("py") + if py_launcher: + try: + out = subprocess.check_output(["py", "-3", "-c", "import sys; print(sys.executable)"], text=True).strip() + if out and not str(Path(out).resolve()).lower().startswith(str(vdir.resolve()).lower()): + return ["py", "-3"] + except Exception: + pass + + for c in candidates: + try: + if isinstance(c, Path) and c.exists(): + c_resolved = Path(c).resolve() + if not str(c_resolved).lower().startswith(str(vdir.resolve()).lower()) and c_resolved != current_exe: + return [str(c_resolved)] + except Exception: + continue + except Exception: + pass + return None + + ext = _find_external_python() + if ext: + cmd = ext + [str(repo_root / "scripts" / "bootstrap.py"), "--uninstall", "--yes"] + if not args.quiet: + print("Attempting uninstall using external Python:", " ".join(cmd)) + rc = subprocess.run(cmd) + if rc.returncode != 0: + print( + f"External uninstall exited with {rc.returncode}; ensure no processes are using files in {vdir} and try again.", + file=sys.stderr, + ) + return int(rc.returncode or 0) + + print( + "Could not find a Python interpreter outside the local .venv. Please deactivate your venv (run 'deactivate') or run the uninstall from a system Python:\n python ./scripts/bootstrap.py --uninstall --yes", + file=sys.stderr, + ) + return 2 + + # Normal (non-venv) uninstall flow: confirm and remove launchers, shims, and venv if not args.yes: try: prompt = input(f"Remove local virtualenv at {vdir} and installed user shims? [y/N]: ") @@ -334,15 +424,19 @@ def main() -> int: return 1 # Remove repo-local launchers - for name in ("mm", "mm.ps1", "mm.bat"): - p = repo_root / name - if p.exists(): + def _remove_launcher(path: Path) -> None: + if path.exists(): try: - p.unlink() + path.unlink() if not args.quiet: - print(f"Removed local launcher: {p}") + print(f"Removed local launcher: {path}") except Exception as exc: - print(f"Warning: failed to remove {p}: {exc}", file=sys.stderr) + print(f"Warning: failed to remove {path}: {exc}", file=sys.stderr) + + scripts_launcher = repo_root / "scripts" / "mm.ps1" + _remove_launcher(scripts_launcher) + for legacy in ("mm", "mm.ps1", "mm.bat"): + _remove_launcher(repo_root / legacy) # Remove user shims that the installer may have written try: @@ -350,12 +444,15 @@ def main() -> int: if system == "windows": user_bin = Path(os.environ.get("USERPROFILE", str(Path.home()))) / "bin" if user_bin.exists(): - for name in ("mm.cmd", "mm.ps1"): + for name in ("mm.ps1",): p = user_bin / name if p.exists(): - p.unlink() - if not args.quiet: - print(f"Removed user shim: {p}") + try: + p.unlink() + if not args.quiet: + print(f"Removed user shim: {p}") + except Exception as exc: + print(f"Warning: failed to remove {p}: {exc}", file=sys.stderr) else: user_bin = Path(os.environ.get("XDG_BIN_HOME", str(Path.home() / ".local/bin"))) if user_bin.exists(): @@ -480,11 +577,37 @@ def main() -> int: except subprocess.CalledProcessError as exc: print(f"Failed to create or prepare local venv: {exc}", file=sys.stderr) raise + + def _ensure_pip_available(python_path: Path) -> None: + """Ensure pip is available inside the venv; fall back to ensurepip if needed.""" + + try: + subprocess.run( + [str(python_path), "-m", "pip", "--version"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + return + except Exception: + pass + + if not args.quiet: + print("Bootstrapping pip inside the local virtualenv...") + try: + run([str(python_path), "-m", "ensurepip", "--upgrade"]) + except subprocess.CalledProcessError as exc: + print( + "Failed to install pip inside the local virtualenv via ensurepip; ensure your Python build includes ensurepip and retry.", + file=sys.stderr, + ) + raise # Ensure a local venv is present and use it for subsequent installs. venv_python = _ensure_local_venv() if not args.quiet: print(f"Using venv python: {venv_python}") + _ensure_pip_available(venv_python) # Enforce opinionated behavior: install deps, playwright, deno, and install project in editable mode. # Ignore `--skip-deps` and `--install-editable` flags to keep the setup deterministic. @@ -531,7 +654,7 @@ def main() -> int: ) if not args.skip_deps: - req_file = repo_root / "requirements.txt" + req_file = repo_root / "scripts" / "requirements.txt" if not req_file.exists(): print( f"requirements.txt not found at {req_file}; skipping dependency installation.", @@ -662,35 +785,15 @@ def main() -> int: print("Deno installation failed.", file=sys.stderr) return rc - # Write project-local launcher scripts (project root) that prefer the local .venv + # Write project-local launcher script under scripts/ to keep the repo root uncluttered. def _write_launchers() -> None: - sh = repo_root / "mm" - ps1 = repo_root / "mm.ps1" - bat = repo_root / "mm.bat" - - sh_text = """#!/usr/bin/env bash -set -e -SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\" -REPO=\"$SCRIPT_DIR\" -VENV=\"$REPO/.venv\" -# Make tools installed into the local venv available in PATH for provider discovery -export PATH=\"$VENV/bin:$PATH\" -PY=\"$VENV/bin/python\" -if [ -x \"$PY\" ]; then - exec \"$PY\" -m medeia_macina.cli_entry \"$@\" -else - exec python -m medeia_macina.cli_entry \"$@\" -fi -""" - try: - sh.write_text(sh_text, encoding="utf-8") - sh.chmod(sh.stat().st_mode | 0o111) - except Exception: - pass + launcher_dir = repo_root / "scripts" + launcher_dir.mkdir(parents=True, exist_ok=True) + ps1 = launcher_dir / "mm.ps1" ps1_text = r"""Param([Parameter(ValueFromRemainingArguments=$true)] $args) $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$repo = $scriptDir +$repo = (Resolve-Path (Join-Path $scriptDir "..")).Path $venv = Join-Path $repo '.venv' # Ensure venv Scripts dir is on PATH for provider discovery $venvScripts = Join-Path $venv 'Scripts' @@ -707,19 +810,6 @@ python -m medeia_macina.cli_entry @args except Exception: pass - bat_text = ( - "@echo off\r\n" - "set SCRIPT_DIR=%~dp0\r\n" - "set PATH=%SCRIPT_DIR%\\.venv\\Scripts;%PATH%\r\n" - 'if exist "%SCRIPT_DIR%\\.venv\\Scripts\\python.exe" "%SCRIPT_DIR%\\.venv\\Scripts\\python.exe" -m medeia_macina.cli_entry %*\r\n' - 'if exist "%SCRIPT_DIR%\\CLI.py" python "%SCRIPT_DIR%\\CLI.py" %*\r\n' - "python -m medeia_macina.cli_entry %*\r\n" - ) - try: - bat.write_text(bat_text, encoding="utf-8") - except Exception: - pass - _write_launchers() # Install user-global shims so `mm` can be executed from any shell session. @@ -732,24 +822,6 @@ python -m medeia_macina.cli_entry @args user_bin = Path(os.environ.get("USERPROFILE", str(home))) / "bin" user_bin.mkdir(parents=True, exist_ok=True) - # Write mm.cmd (CMD shim) - mm_cmd = user_bin / "mm.cmd" - cmd_text = ( - f"@echo off\r\n" - f"set REPO={repo}\r\n" - f'if exist "%REPO%\\.venv\\Scripts\\mm.exe" "%REPO%\\.venv\\Scripts\\mm.exe" %*\r\n' - f"if defined MM_DEBUG (\r\n" - f" echo MM_DEBUG: REPO=%REPO%\r\n" - f' if exist "%REPO%\\.venv\\Scripts\\python.exe" "%REPO%\\.venv\\Scripts\\python.exe" -c "import sys,importlib,importlib.util; print(\'sys.executable:\', sys.executable); print(\'sys.path (first 8):\', sys.path[:8]);" \r\n' - f")\r\n" - f'if exist "%REPO%\\.venv\\Scripts\\python.exe" "%REPO%\\.venv\\Scripts\\python.exe" -m medeia_macina.cli_entry %*\r\n' - f"python -m medeia_macina.cli_entry %*\r\n" - ) - if mm_cmd.exists(): - bak = mm_cmd.with_suffix(f".bak{int(time.time())}") - mm_cmd.replace(bak) - mm_cmd.write_text(cmd_text, encoding="utf-8") - # Write mm.ps1 (PowerShell shim) mm_ps1 = user_bin / "mm.ps1" ps1_text = ( diff --git a/scripts/hydrusnetwork.py b/scripts/hydrusnetwork.py index d5c6a56..7776645 100644 --- a/scripts/hydrusnetwork.py +++ b/scripts/hydrusnetwork.py @@ -10,7 +10,7 @@ Works on Linux and Windows. Behavior: 2) Update hydrus (git pull) 3) Re-clone (remove and re-clone) - If `git` is not available, the script will fall back to downloading the repository ZIP and extracting it. -- By default the script will create a repository-local virtual environment `.//.venv` after cloning/extraction; use `--no-venv` to skip this. By default the script will install dependencies from `requirements.txt` into that venv (use `--no-install-deps` to skip). After setup the script will print instructions for running the client; use `--run-client` to *launch* `hydrus_client.py` using the created repo venv's Python (use `--run-client-detached` to run it in the background). +- By default the script will create a repository-local virtual environment `.//.venv` after cloning/extraction; use `--no-venv` to skip this. By default the script will install dependencies from `scripts/requirements.txt` into that venv (use `--no-install-deps` to skip). After setup the script will print instructions for running the client; use `--run-client` to *launch* `hydrus_client.py` using the created repo venv's Python (use `--run-client-detached` to run it in the background). Examples: python scripts/hydrusnetwork.py @@ -510,17 +510,18 @@ def fix_permissions( def find_requirements(root: Path) -> Optional[Path]: - """Return a requirements.txt Path if found in common locations (root, client, requirements) or via a shallow search. + """Return a requirements.txt Path if found in common locations (scripts, root, client, requirements) or via a shallow search. This tries a few sensible locations used by various projects and performs a shallow two-level walk to find a requirements.txt so installation works even if the file is not at the repository root. """ - candidates = [ - root / "requirements.txt", - root / "client" / "requirements.txt", - root / "requirements" / "requirements.txt", - ] + candidates = [ + root / "scripts" / "requirements.txt", + root / "requirements.txt", + root / "client" / "requirements.txt", + root / "requirements" / "requirements.txt", + ] for c in candidates: if c.exists(): return c diff --git a/requirements-dev.txt b/scripts/requirements-dev.txt similarity index 100% rename from requirements-dev.txt rename to scripts/requirements-dev.txt diff --git a/requirements.txt b/scripts/requirements.txt similarity index 71% rename from requirements.txt rename to scripts/requirements.txt index 3b0bfbd..b02bdbb 100644 --- a/requirements.txt +++ b/scripts/requirements.txt @@ -8,7 +8,10 @@ textual>=0.30.0 yt-dlp[default]>=2023.11.0 requests>=2.31.0 httpx>=0.25.0 -telethon>=1.36.0 +# Ensure requests can detect encodings and ship certificates +charset-normalizer>=3.2.0 +certifi>=2024.12.0 +# Optional Telegram support installs telethon>=1.36.0 when [provider=telegram] is configured. internetarchive>=4.1.0 # Document and data handling @@ -22,18 +25,12 @@ Pillow>=10.0.0 python-bidi>=0.4.2 ffmpeg-python>=0.2.0 -# AI tagging (FlorenceVision tool) -transformers>=4.45.0 -torch>=2.4.0 -einops>=0.8.0 -timm>=1.0.0 - # Metadata extraction and processing musicbrainzngs>=0.7.0 lxml>=4.9.0 # Advanced searching and libraries -aioslsk>=1.6.0 +# Optional Soulseek support installs aioslsk>=1.6.0 when [provider=soulseek] is configured. imdbinfo>=0.1.10 # Encryption and security (if needed by Crypto usage) diff --git a/scripts/run_client.py b/scripts/run_client.py index cc379da..ccb861d 100644 --- a/scripts/run_client.py +++ b/scripts/run_client.py @@ -7,7 +7,7 @@ present or its copy of this helper gets removed. Features (subset of the repo helper): - Locate repository venv (default: /hydrusnetwork/.venv) -- Install or reinstall requirements.txt into the venv +- Install or reinstall scripts/requirements.txt into the venv - Verify key imports - Launch hydrus_client.py (foreground or detached) - Install/uninstall simple user-level start-on-boot services (schtasks/systemd/crontab) @@ -50,7 +50,7 @@ def get_python_in_venv(venv_dir: Path) -> Optional[Path]: def find_requirements(root: Path) -> Optional[Path]: - candidates = [root / "requirements.txt", root / "client" / "requirements.txt"] + candidates = [root / "scripts" / "requirements.txt", root / "requirements.txt", root / "client" / "requirements.txt"] for c in candidates: if c.exists(): return c