This commit is contained in:
2026-01-09 13:41:18 -08:00
parent 1deddfda5c
commit 94aca4d3d4
2 changed files with 124 additions and 32 deletions

View File

@@ -136,11 +136,16 @@ def playwright_package_installed() -> bool:
def _build_playwright_install_cmd(browsers: str | None) -> list[str]: def _build_playwright_install_cmd(browsers: str | None) -> list[str]:
"""Return the command to install Playwright browsers. """Return the command to install Playwright browsers.
- If browsers is None or empty: default to install Chromium only. - If browsers is None or empty: default to install Chromium only (headless).
- If browsers contains 'all': install all engines by running 'playwright install' with no extra args. - If browsers contains 'all': install all engines by running 'playwright install' with no extra args.
- Otherwise, validate entries and return a command that installs the named engines. - Otherwise, validate entries and return a command that installs the named engines.
The --with-deps flag is NOT used because:
1. The project already includes ffmpeg (in MPV/ffmpeg)
2. Most system dependencies should already be available
""" """
# Use --skip-browsers to just install deps without browsers, then install specific browsers
base = [sys.executable, "-m", "playwright", "install"] base = [sys.executable, "-m", "playwright", "install"]
if not browsers: if not browsers:
return base + ["chromium"] return base + ["chromium"]
@@ -293,7 +298,10 @@ def main() -> int:
) )
args = parser.parse_args() args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent # Ensure repo_root is always the project root, not the current working directory
# This prevents issues when bootstrap.py is run from different directories
script_dir = Path(__file__).resolve().parent
repo_root = script_dir.parent
# Helpers for interactive menu and uninstall detection # Helpers for interactive menu and uninstall detection
def _venv_python_path(p: Path) -> Path | None: def _venv_python_path(p: Path) -> Path | None:
@@ -795,15 +803,19 @@ def main() -> int:
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repo = (Resolve-Path (Join-Path $scriptDir "..")).Path $repo = (Resolve-Path (Join-Path $scriptDir "..")).Path
$venv = Join-Path $repo '.venv' $venv = Join-Path $repo '.venv'
$py = Join-Path $venv 'Scripts\python.exe'
if (Test-Path $py) {
& $py -m scripts.cli_entry @args; exit $LASTEXITCODE
}
# Ensure venv Scripts dir is on PATH for provider discovery # Ensure venv Scripts dir is on PATH for provider discovery
$venvScripts = Join-Path $venv 'Scripts' $venvScripts = Join-Path $venv 'Scripts'
if (Test-Path $venvScripts) { $env:PATH = $venvScripts + ';' + $env:PATH } if (Test-Path $venvScripts) { $env:PATH = $venvScripts + ';' + $env:PATH }
$py = Join-Path $venv 'Scripts\python.exe' # Fallback to system python if venv doesn't exist
$cli = Join-Path $repo 'CLI.py' if (Test-Path (Join-Path $repo 'CLI.py')) {
if (Test-Path $py) { & $py -m scripts.cli_entry @args; exit $LASTEXITCODE } python -m scripts.cli_entry @args
if (Test-Path $cli) { & python $cli @args; exit $LASTEXITCODE } } else {
# fallback python -m scripts.cli_entry @args
python -m scripts.cli_entry @args }
""" """
try: try:
ps1.write_text(ps1_text, encoding="utf-8") ps1.write_text(ps1_text, encoding="utf-8")
@@ -828,16 +840,16 @@ python -m scripts.cli_entry @args
"Param([Parameter(ValueFromRemainingArguments=$true)] $args)\n" "Param([Parameter(ValueFromRemainingArguments=$true)] $args)\n"
f'$repo = "{repo}"\n' f'$repo = "{repo}"\n'
"$venv = Join-Path $repo '.venv'\n" "$venv = Join-Path $repo '.venv'\n"
"$exe = Join-Path $venv 'Scripts\\mm.exe'\n"
"if (Test-Path $exe) { & $exe @args; exit $LASTEXITCODE }\n"
"$py = Join-Path $venv 'Scripts\\python.exe'\n" "$py = Join-Path $venv 'Scripts\\python.exe'\n"
"if (Test-Path $py) {\n" "if (Test-Path $py) {\n"
" if ($env:MM_DEBUG) {\n" " if ($env:MM_DEBUG) {\n"
' Write-Host "MM_DEBUG: diagnostics" -ForegroundColor Yellow\n' ' Write-Host "MM_DEBUG: using venv python at $py" -ForegroundColor Yellow\n'
" & $py -c \"import sys,importlib,importlib.util,traceback; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);\"\n" " & $py -c \"import sys; print('sys.executable:', sys.executable)\"\n"
" }\n" " }\n"
" & $py -m scripts.cli_entry @args; exit $LASTEXITCODE\n" " & $py -m scripts.cli_entry @args; exit $LASTEXITCODE\n"
"}\n" "}\n"
"# Fallback to system python if venv doesn't exist\n"
"if ($env:MM_DEBUG) { Write-Host 'MM_DEBUG: venv python not found, trying system python' -ForegroundColor Yellow }\n"
"python -m scripts.cli_entry @args\n" "python -m scripts.cli_entry @args\n"
) )
if mm_ps1.exists(): if mm_ps1.exists():
@@ -845,29 +857,71 @@ python -m scripts.cli_entry @args
mm_ps1.replace(bak) mm_ps1.replace(bak)
mm_ps1.write_text(ps1_text, encoding="utf-8") mm_ps1.write_text(ps1_text, encoding="utf-8")
# Attempt to add user_bin to the user's PATH if it's not present. # Write mm.bat (CMD shim for better compatibility)
try: mm_bat = user_bin / "mm.bat"
cur = os.environ.get("PATH", "") bat_text = (
"@echo off\n"
"setlocal enabledelayedexpansion\n"
f'set "REPO={repo}"\n'
"set \"VENV=!REPO!\\.venv\"\n"
"set \"PY=!VENV!\\Scripts\\python.exe\"\n"
"if exist \"!PY!\" (\n"
" if defined MM_DEBUG (\n"
" echo MM_DEBUG: using venv python at !PY!\n"
" \"!PY!\" -c \"import sys; print('sys.executable:', sys.executable)\"\n"
" )\n"
" \"!PY!\" -m scripts.cli_entry %*\n"
" exit /b !ERRORLEVEL!\n"
")\n"
"echo MM: venv not found at !VENV!, trying system python\n"
"if defined MM_DEBUG echo MM_DEBUG: venv python not found, trying system python\n"
"python -m scripts.cli_entry %*\n"
"exit /b !ERRORLEVEL!\n"
)
if mm_bat.exists():
bak = mm_bat.with_suffix(f".bak{int(time.time())}")
mm_bat.replace(bak)
mm_bat.write_text(bat_text, encoding="utf-8")
# Add user_bin to PATH for current and future sessions
str_bin = str(user_bin) str_bin = str(user_bin)
if str_bin not in cur: cur_path = os.environ.get("PATH", "")
# Update current session PATH if not already present
if str_bin not in cur_path:
os.environ["PATH"] = str_bin + ";" + cur_path
if not args.quiet:
print(f"Added {user_bin} to current session PATH")
# Persist to user's Windows registry PATH for future sessions
try:
ps_cmd = ( ps_cmd = (
"$bin = '{bin}';" "$bin = '{bin}';"
"$cur = [Environment]::GetEnvironmentVariable('PATH','User');" "$cur = [Environment]::GetEnvironmentVariable('PATH','User');"
"if ($cur -notlike \"*$bin*\") {[Environment]::SetEnvironmentVariable('PATH', ($bin + ';' + ($cur -ne $null ? $cur : '')), 'User')}" "if ($cur -notlike \"*$bin*\") {{"
).format(bin=str_bin.replace("\\", "[Environment]::SetEnvironmentVariable('PATH', ($bin + ';' + ($cur -ne $null ? $cur : '')), 'User');"
"\\\\")) "Write-Host 'Added {bin} to User PATH in registry' -ForegroundColor Green"
"}}"
).format(bin=str_bin.replace("\\", "\\\\"))
subprocess.run( subprocess.run(
["powershell", ["powershell",
"-NoProfile", "-NoProfile",
"-Command", "-Command",
ps_cmd], ps_cmd],
check=False check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
) )
except Exception: except Exception as e:
pass if not args.quiet:
print(f"Note: Could not persist PATH to registry (this is non-critical): {e}", file=sys.stderr)
if not args.quiet: if not args.quiet:
print(f"Installed global launchers to: {user_bin}") print(f"\nInstalled global launchers to: {user_bin}")
print(f"✓ mm.ps1 (PowerShell)")
print(f"✓ mm.bat (Command Prompt)")
print(f"\nYou can now run 'mm' from any terminal window.")
print(f"To use in the current terminal, reload your profile or run: $env:PATH = '{str_bin};' + $env:PATH")
else: else:
# POSIX # POSIX

View File

@@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
import contextlib import contextlib
import os
import re import re
import shutil
import tempfile import tempfile
import traceback import traceback
from dataclasses import dataclass from dataclasses import dataclass
@@ -74,6 +76,7 @@ class PlaywrightDefaults:
viewport_height: int = 1080 viewport_height: int = 1080
navigation_timeout_ms: int = 90_000 navigation_timeout_ms: int = 90_000
ignore_https_errors: bool = True ignore_https_errors: bool = True
ffmpeg_path: Optional[str] = None # Path to ffmpeg executable; auto-detected if None
@dataclass(slots=True) @dataclass(slots=True)
@@ -101,6 +104,7 @@ class PlaywrightTool:
- sync_playwright start/stop - sync_playwright start/stop
- browser launch/context creation - browser launch/context creation
- user-agent/viewport defaults - user-agent/viewport defaults
- ffmpeg path resolution (for video recording)
Config overrides (top-level keys): Config overrides (top-level keys):
- playwright.browser="chromium" - playwright.browser="chromium"
@@ -110,6 +114,13 @@ class PlaywrightTool:
- playwright.viewport_height=1200 - playwright.viewport_height=1200
- playwright.navigation_timeout_ms=90000 - playwright.navigation_timeout_ms=90000
- playwright.ignore_https_errors=true - playwright.ignore_https_errors=true
- playwright.ffmpeg_path="/path/to/ffmpeg" (auto-detected if not set)
FFmpeg resolution (in order):
1. Config key: playwright.ffmpeg_path
2. Environment variable: PLAYWRIGHT_FFMPEG_PATH
3. Project bundled: MPV/ffmpeg/bin/ffmpeg[.exe]
4. System PATH: which ffmpeg
""" """
def __init__(self, config: Optional[Dict[str, Any]] = None) -> None: def __init__(self, config: Optional[Dict[str, Any]] = None) -> None:
@@ -162,6 +173,32 @@ class PlaywrightTool:
ignore_https = bool(_get("ignore_https_errors", defaults.ignore_https_errors)) ignore_https = bool(_get("ignore_https_errors", defaults.ignore_https_errors))
# Try to find ffmpeg: config override, environment variable, bundled, then system
ffmpeg_path: Optional[str] = None
config_ffmpeg = _get("ffmpeg_path", None)
if config_ffmpeg:
ffmpeg_path = str(config_ffmpeg).strip()
else:
# Check environment variable (supports project ffmpeg)
env_ffmpeg = os.environ.get("PLAYWRIGHT_FFMPEG_PATH")
if env_ffmpeg:
ffmpeg_path = env_ffmpeg
else:
# Try to find bundled ffmpeg in the project (if available)
try:
repo_root = Path(__file__).resolve().parent.parent
bundled_ffmpeg = repo_root / "MPV" / "ffmpeg" / "bin"
if bundled_ffmpeg.exists():
ffmpeg_exe = bundled_ffmpeg / ("ffmpeg.exe" if os.name == "nt" else "ffmpeg")
if ffmpeg_exe.exists():
ffmpeg_path = str(ffmpeg_exe)
except Exception:
pass
# Try system ffmpeg if bundled not found
if not ffmpeg_path:
ffmpeg_path = shutil.which("ffmpeg")
return PlaywrightDefaults( return PlaywrightDefaults(
browser=browser, browser=browser,
headless=headless, headless=headless,
@@ -170,6 +207,7 @@ class PlaywrightTool:
viewport_height=vh, viewport_height=vh,
navigation_timeout_ms=nav_timeout, navigation_timeout_ms=nav_timeout,
ignore_https_errors=ignore_https, ignore_https_errors=ignore_https,
ffmpeg_path=ffmpeg_path,
) )
def require(self) -> None: def require(self) -> None: