Skip to content

Commit c47f01f

Browse files
authored
Merge pull request teng-lin#204 from chkp-roniz/feat/edge-browser-login
feat(cli): add --browser flag to login for Microsoft Edge SSO support
2 parents 8f9dd3a + 4aba7c3 commit c47f01f

File tree

4 files changed

+151
-17
lines changed

4 files changed

+151
-17
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ pip install git+https://github.com/teng-lin/notebooklm-py@main
118118
```bash
119119
# 1. Authenticate (opens browser)
120120
notebooklm login
121+
# Or use Microsoft Edge (for orgs that require Edge for SSO)
122+
# notebooklm login --browser msedge
121123

122124
# 2. Create a notebook and add sources
123125
notebooklm create "My Research"

docs/cli-reference.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ See [Configuration](configuration.md) for details on environment variables and C
3838

3939
| Command | Description | Example |
4040
|---------|-------------|---------|
41-
| `login` | Authenticate via browser | `notebooklm login` |
41+
| `login` | Authenticate via browser | `notebooklm login` / `notebooklm login --browser msedge` |
4242
| `use <id>` | Set active notebook | `notebooklm use abc123` |
4343
| `status` | Show current context | `notebooklm status` |
4444
| `status --paths` | Show configuration paths | `notebooklm status --paths` |
@@ -232,11 +232,24 @@ These CLI capabilities are not available in NotebookLM's web interface:
232232
Authenticate with Google NotebookLM via browser.
233233

234234
```bash
235-
notebooklm login
235+
notebooklm login [OPTIONS]
236236
```
237237

238238
Opens a Chromium browser with a persistent profile. Log in to your Google account, then press Enter in the terminal to save the session.
239239

240+
**Options:**
241+
- `--storage PATH` - Where to save storage_state.json (default: `$NOTEBOOKLM_HOME/storage_state.json`)
242+
- `--browser [chromium|msedge]` - Browser to use for login (default: `chromium`). Use `msedge` for Microsoft Edge.
243+
244+
**Examples:**
245+
```bash
246+
# Default (Chromium)
247+
notebooklm login
248+
249+
# Use Microsoft Edge (for orgs that require Edge for SSO)
250+
notebooklm login --browser msedge
251+
```
252+
240253
### Session: `use`
241254

242255
Set the active notebook for subsequent commands.

src/notebooklm/cli/session.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,20 @@ def register_session_commands(cli):
160160
default=None,
161161
help="Where to save storage_state.json (default: $NOTEBOOKLM_HOME/storage_state.json)",
162162
)
163-
def login(storage):
163+
@click.option(
164+
"--browser",
165+
type=click.Choice(["chromium", "msedge"], case_sensitive=False),
166+
default="chromium",
167+
help="Browser to use for login (default: chromium). Use 'msedge' for Microsoft Edge.",
168+
)
169+
def login(storage, browser):
164170
"""Log in to NotebookLM via browser.
165171
166172
Opens a browser window for Google login. After logging in,
167173
press ENTER in the terminal to save authentication.
168174
175+
Use --browser msedge if your organization requires Microsoft Edge for SSO.
176+
169177
Note: Cannot be used when NOTEBOOKLM_AUTH_JSON is set (use file-based
170178
auth or unset the env var first).
171179
"""
@@ -184,36 +192,56 @@ def login(storage):
184192
try:
185193
from playwright.sync_api import sync_playwright
186194
except ImportError:
187-
console.print(
188-
"[red]Playwright not installed. Run:[/red]\n"
189-
" pip install notebooklm[browser]\n"
190-
" playwright install chromium"
191-
)
195+
if browser == "msedge":
196+
install_hint = " pip install notebooklm[browser]"
197+
else:
198+
install_hint = " pip install notebooklm[browser]\n playwright install chromium"
199+
console.print(f"[red]Playwright not installed. Run:[/red]\n{install_hint}")
192200
raise SystemExit(1) from None
193201

194-
# Pre-flight check: verify Chromium browser is installed
195-
_ensure_chromium_installed()
202+
# Pre-flight check: verify Chromium browser is installed (skip for Edge)
203+
if browser == "chromium":
204+
_ensure_chromium_installed()
196205

197206
storage_path = Path(storage) if storage else get_storage_path()
198207
browser_profile = get_browser_profile_dir()
199208
storage_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
200209
browser_profile.mkdir(parents=True, exist_ok=True, mode=0o700)
201210

202-
console.print("[yellow]Opening browser for Google login...[/yellow]")
211+
browser_label = "Microsoft Edge" if browser == "msedge" else "Chromium"
212+
console.print(f"[yellow]Opening {browser_label} for Google login...[/yellow]")
203213
console.print(f"[dim]Using persistent profile: {browser_profile}[/dim]")
204214

205215
# Use context manager to restore ProactorEventLoop for Playwright on Windows
206216
# (fixes #89: NotImplementedError on Windows Python 3.12)
207217
with _windows_playwright_event_loop(), sync_playwright() as p:
208-
context = p.chromium.launch_persistent_context(
209-
user_data_dir=str(browser_profile),
210-
headless=False,
211-
args=[
218+
launch_kwargs: dict[str, Any] = {
219+
"user_data_dir": str(browser_profile),
220+
"headless": False,
221+
"args": [
212222
"--disable-blink-features=AutomationControlled",
213223
"--password-store=basic", # Avoid macOS keychain encryption for headless compatibility
214224
],
215-
ignore_default_args=["--enable-automation"],
216-
)
225+
"ignore_default_args": ["--enable-automation"],
226+
}
227+
if browser == "msedge":
228+
launch_kwargs["channel"] = "msedge"
229+
230+
try:
231+
context = p.chromium.launch_persistent_context(**launch_kwargs)
232+
except Exception as e:
233+
if browser == "msedge" and (
234+
"executable doesn't exist" in str(e).lower()
235+
or "no such file" in str(e).lower()
236+
or "failed to launch" in str(e).lower()
237+
):
238+
console.print(
239+
"[red]Microsoft Edge not found.[/red]\n"
240+
"Install from: https://www.microsoft.com/edge\n"
241+
"Or use the default Chromium browser: notebooklm login"
242+
)
243+
raise SystemExit(1) from None
244+
raise
217245

218246
page = context.pages[0] if context.pages else context.new_page()
219247
page.goto(NOTEBOOKLM_URL)

tests/unit/cli/test_session.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,97 @@ def test_login_blocked_when_notebooklm_auth_json_set(self, runner, monkeypatch):
8383
assert result.exit_code == 1
8484
assert "Cannot run 'login' when NOTEBOOKLM_AUTH_JSON is set" in result.output
8585

86+
def test_login_help_shows_browser_option(self, runner):
87+
"""Test login --help shows --browser option with chromium/msedge choices."""
88+
result = runner.invoke(cli, ["login", "--help"])
89+
90+
assert result.exit_code == 0
91+
assert "--browser" in result.output
92+
assert "chromium" in result.output
93+
assert "msedge" in result.output
94+
95+
def test_login_rejects_invalid_browser(self, runner):
96+
"""Test login rejects invalid --browser values."""
97+
result = runner.invoke(cli, ["login", "--browser", "firefox"])
98+
99+
assert result.exit_code != 0
100+
101+
@pytest.fixture
102+
def mock_login_browser(self, tmp_path):
103+
"""Mock Playwright browser launch for login --browser tests.
104+
105+
Yields (mock_ensure, mock_launch) for assertions on chromium install
106+
check and launch_persistent_context kwargs.
107+
"""
108+
with (
109+
patch("notebooklm.cli.session._ensure_chromium_installed") as mock_ensure,
110+
patch("playwright.sync_api.sync_playwright") as mock_pw,
111+
patch(
112+
"notebooklm.cli.session.get_storage_path", return_value=tmp_path / "storage.json"
113+
),
114+
patch(
115+
"notebooklm.cli.session.get_browser_profile_dir",
116+
return_value=tmp_path / "profile",
117+
),
118+
patch("notebooklm.cli.session._sync_server_language_to_config"),
119+
patch("builtins.input", return_value=""),
120+
):
121+
mock_context = MagicMock()
122+
mock_page = MagicMock()
123+
mock_page.url = "https://notebooklm.google.com/"
124+
mock_context.pages = [mock_page]
125+
mock_launch = (
126+
mock_pw.return_value.__enter__.return_value.chromium.launch_persistent_context
127+
)
128+
mock_launch.return_value = mock_context
129+
130+
yield mock_ensure, mock_launch
131+
132+
def test_login_msedge_skips_chromium_install(self, runner, mock_login_browser):
133+
"""Test --browser msedge skips _ensure_chromium_installed."""
134+
mock_ensure, _ = mock_login_browser
135+
runner.invoke(cli, ["login", "--browser", "msedge"])
136+
mock_ensure.assert_not_called()
137+
138+
def test_login_msedge_passes_channel_param(self, runner, mock_login_browser):
139+
"""Test --browser msedge passes channel='msedge' to launch_persistent_context."""
140+
_, mock_launch = mock_login_browser
141+
runner.invoke(cli, ["login", "--browser", "msedge"])
142+
assert mock_launch.call_args[1].get("channel") == "msedge"
143+
144+
def test_login_chromium_default_no_channel(self, runner, mock_login_browser):
145+
"""Test default chromium calls _ensure_chromium_installed and has no channel."""
146+
mock_ensure, mock_launch = mock_login_browser
147+
runner.invoke(cli, ["login", "--browser", "chromium"])
148+
mock_ensure.assert_called_once()
149+
assert "channel" not in mock_launch.call_args[1]
150+
151+
def test_login_msedge_not_installed_shows_helpful_error(self, runner, tmp_path):
152+
"""Test --browser msedge shows helpful error when Edge is not installed."""
153+
with (
154+
patch("notebooklm.cli.session._ensure_chromium_installed"),
155+
patch("playwright.sync_api.sync_playwright") as mock_pw,
156+
patch(
157+
"notebooklm.cli.session.get_storage_path", return_value=tmp_path / "storage.json"
158+
),
159+
patch(
160+
"notebooklm.cli.session.get_browser_profile_dir",
161+
return_value=tmp_path / "profile",
162+
),
163+
):
164+
mock_launch = (
165+
mock_pw.return_value.__enter__.return_value.chromium.launch_persistent_context
166+
)
167+
mock_launch.side_effect = Exception(
168+
"Executable doesn't exist at /ms-edge\nFailed to launch"
169+
)
170+
171+
result = runner.invoke(cli, ["login", "--browser", "msedge"])
172+
173+
assert result.exit_code == 1
174+
assert "Microsoft Edge not found" in result.output
175+
assert "microsoft.com/edge" in result.output
176+
86177

87178
# =============================================================================
88179
# USE COMMAND TESTS

0 commit comments

Comments
 (0)