Skip to content

Commit 1956282

Browse files
nodeeeeeeclaude
andcommitted
Auto-install Playwright Chromium browser on first use
When Playwright's Chromium binary is missing, the downloader now automatically runs `playwright install chromium` instead of crashing with a confusing error asking the user to install it manually. The check runs once per process and is called before every sync_playwright() usage (3 call sites in downloader.py). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fd0a1d7 commit 1956282

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

downloader.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,62 @@ def _canvas_headers() -> dict:
102102
return {"Authorization": f"Bearer {CANVAS_TOKEN}"}
103103

104104

105+
_playwright_checked = False
106+
107+
def _ensure_playwright_browsers() -> None:
108+
"""Auto-install Chromium browser for Playwright if not already present.
109+
110+
Called once per process — checks if the Chromium binary exists, and
111+
if not, runs `playwright install chromium` automatically so the user
112+
never has to do it themselves.
113+
"""
114+
global _playwright_checked
115+
if _playwright_checked:
116+
return
117+
_playwright_checked = True
118+
119+
try:
120+
from playwright._impl._driver import compute_driver_executable
121+
driver_exec = compute_driver_executable()
122+
import subprocess as _sp
123+
result = _sp.run(
124+
[str(driver_exec), "install", "--dry-run", "chromium"],
125+
capture_output=True, text=True, timeout=10,
126+
)
127+
# If dry-run succeeds without mentioning "not installed", we're good
128+
if result.returncode == 0:
129+
return
130+
except Exception:
131+
pass
132+
133+
# Check by actually trying to find the executable path
134+
try:
135+
from playwright.sync_api import sync_playwright as _sp_check
136+
with _sp_check() as p:
137+
p.chromium.executable_path # noqa: B018
138+
return # browser exists
139+
except Exception:
140+
pass
141+
142+
# Browser not found — install it
143+
tqdm.write(" [playwright] Chromium not found — installing automatically…")
144+
import subprocess as _sp
145+
try:
146+
proc = _sp.run(
147+
[sys.executable, "-m", "playwright", "install", "chromium"],
148+
capture_output=True, text=True, timeout=300,
149+
)
150+
if proc.returncode == 0:
151+
tqdm.write(" [playwright] Chromium installed successfully.")
152+
else:
153+
tqdm.write(f" [playwright] Install exited with code {proc.returncode}")
154+
if proc.stderr:
155+
tqdm.write(f" [playwright] {proc.stderr.strip()[:200]}")
156+
except Exception as e:
157+
tqdm.write(f" [playwright] Auto-install failed: {e}")
158+
tqdm.write(" [playwright] Run manually: python -m playwright install chromium")
159+
160+
105161
def _is_academic(course) -> bool:
106162
try:
107163
name = course.name.lower()
@@ -259,6 +315,7 @@ def _get_panopto_tab_folder(course_id: int) -> tuple[str | None, list[dict], str
259315
Returns (folder_id, panopto_cookies, bearer_token).
260316
"""
261317
global PANOPTO_HOST # may be auto-detected from Playwright request URLs
318+
_ensure_playwright_browsers()
262319
from playwright.sync_api import sync_playwright
263320

264321
r = requests.get(
@@ -550,6 +607,7 @@ def _get_sessionless_launch_url(course_id: int, item_id: int) -> str | None:
550607

551608

552609
def _get_panopto_session(launch_url: str) -> tuple[str | None, list[dict]]:
610+
_ensure_playwright_browsers()
553611
from playwright.sync_api import sync_playwright
554612
session_id, cookies = None, []
555613
with sync_playwright() as p:
@@ -667,6 +725,7 @@ def _extract_stream(body: dict) -> tuple[str, str] | None:
667725
if not launch_url:
668726
return None
669727

728+
_ensure_playwright_browsers()
670729
from playwright.sync_api import sync_playwright
671730

672731
viewer_page_url = f"https://{PANOPTO_HOST}/Panopto/Pages/Viewer.aspx?id={session_id}"

0 commit comments

Comments
 (0)