diff --git a/scripts/bootstrap.py b/scripts/bootstrap.py index 0d203f3..179a3ae 100644 --- a/scripts/bootstrap.py +++ b/scripts/bootstrap.py @@ -136,11 +136,16 @@ def playwright_package_installed() -> bool: def _build_playwright_install_cmd(browsers: str | None) -> list[str]: """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. - 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"] if not browsers: return base + ["chromium"] @@ -293,7 +298,10 @@ def main() -> int: ) 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 def _venv_python_path(p: Path) -> Path | None: @@ -795,15 +803,19 @@ def main() -> int: $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repo = (Resolve-Path (Join-Path $scriptDir "..")).Path $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 $venvScripts = Join-Path $venv 'Scripts' if (Test-Path $venvScripts) { $env:PATH = $venvScripts + ';' + $env:PATH } -$py = Join-Path $venv 'Scripts\python.exe' -$cli = Join-Path $repo 'CLI.py' -if (Test-Path $py) { & $py -m scripts.cli_entry @args; exit $LASTEXITCODE } -if (Test-Path $cli) { & python $cli @args; exit $LASTEXITCODE } -# fallback -python -m scripts.cli_entry @args +# Fallback to system python if venv doesn't exist +if (Test-Path (Join-Path $repo 'CLI.py')) { + python -m scripts.cli_entry @args +} else { + python -m scripts.cli_entry @args +} """ try: ps1.write_text(ps1_text, encoding="utf-8") @@ -828,16 +840,16 @@ python -m scripts.cli_entry @args "Param([Parameter(ValueFromRemainingArguments=$true)] $args)\n" f'$repo = "{repo}"\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" "if (Test-Path $py) {\n" " if ($env:MM_DEBUG) {\n" - ' Write-Host "MM_DEBUG: diagnostics" -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" + ' Write-Host "MM_DEBUG: using venv python at $py" -ForegroundColor Yellow\n' + " & $py -c \"import sys; print('sys.executable:', sys.executable)\"\n" " }\n" " & $py -m scripts.cli_entry @args; exit $LASTEXITCODE\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" ) if mm_ps1.exists(): @@ -845,29 +857,71 @@ python -m scripts.cli_entry @args mm_ps1.replace(bak) 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) + mm_bat = user_bin / "mm.bat" + 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) + 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: - cur = os.environ.get("PATH", "") - str_bin = str(user_bin) - if str_bin not in cur: - ps_cmd = ( - "$bin = '{bin}';" - "$cur = [Environment]::GetEnvironmentVariable('PATH','User');" - "if ($cur -notlike \"*$bin*\") {[Environment]::SetEnvironmentVariable('PATH', ($bin + ';' + ($cur -ne $null ? $cur : '')), 'User')}" - ).format(bin=str_bin.replace("\\", - "\\\\")) - subprocess.run( - ["powershell", - "-NoProfile", - "-Command", - ps_cmd], - check=False - ) - except Exception: - pass + ps_cmd = ( + "$bin = '{bin}';" + "$cur = [Environment]::GetEnvironmentVariable('PATH','User');" + "if ($cur -notlike \"*$bin*\") {{" + "[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( + ["powershell", + "-NoProfile", + "-Command", + ps_cmd], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + except Exception as e: + 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: - 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: # POSIX diff --git a/tool/playwright.py b/tool/playwright.py index d4eaa0a..48de954 100644 --- a/tool/playwright.py +++ b/tool/playwright.py @@ -1,7 +1,9 @@ from __future__ import annotations import contextlib +import os import re +import shutil import tempfile import traceback from dataclasses import dataclass @@ -74,6 +76,7 @@ class PlaywrightDefaults: viewport_height: int = 1080 navigation_timeout_ms: int = 90_000 ignore_https_errors: bool = True + ffmpeg_path: Optional[str] = None # Path to ffmpeg executable; auto-detected if None @dataclass(slots=True) @@ -101,6 +104,7 @@ class PlaywrightTool: - sync_playwright start/stop - browser launch/context creation - user-agent/viewport defaults + - ffmpeg path resolution (for video recording) Config overrides (top-level keys): - playwright.browser="chromium" @@ -110,6 +114,13 @@ class PlaywrightTool: - playwright.viewport_height=1200 - playwright.navigation_timeout_ms=90000 - 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: @@ -162,6 +173,32 @@ class PlaywrightTool: 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( browser=browser, headless=headless, @@ -170,6 +207,7 @@ class PlaywrightTool: viewport_height=vh, navigation_timeout_ms=nav_timeout, ignore_https_errors=ignore_https, + ffmpeg_path=ffmpeg_path, ) def require(self) -> None: