@@ -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+
105161def _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
552609def _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