notebooklm-api-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dockerignore ADDED
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ .wrangler
4
+ *.md
5
+ *.toml
6
+ tsconfig.json
7
+ src
package/Dockerfile ADDED
@@ -0,0 +1,9 @@
1
+ FROM python:3.12-slim
2
+
3
+ RUN pip install --no-cache-dir notebooklm-py fastapi uvicorn
4
+
5
+ WORKDIR /app
6
+ COPY container/server.py /app/server.py
7
+
8
+ EXPOSE 8080
9
+ CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"]
@@ -0,0 +1,123 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import tempfile
5
+ from pathlib import Path
6
+ from fastapi import FastAPI, HTTPException, Request
7
+ from pydantic import BaseModel
8
+
9
+ app = FastAPI()
10
+
11
+ AUTH_FILE = Path("/app/.auth.json")
12
+ AUTH_JSON = os.environ.get("NOTEBOOKLM_AUTH_JSON", "")
13
+
14
+ if AUTH_JSON and not AUTH_FILE.exists():
15
+ AUTH_FILE.write_text(AUTH_JSON)
16
+
17
+
18
+ class Job(BaseModel):
19
+ action: str
20
+ notebookId: str | None = None
21
+ sourceUrls: list[str] | None = None
22
+ title: str | None = None
23
+ prompt: str | None = None
24
+
25
+
26
+ def run_cmd(args: list[str], cwd: str) -> str:
27
+ env = os.environ.copy()
28
+ env["NOTEBOOKLM_AUTH_JSON"] = AUTH_JSON
29
+ result = subprocess.run(
30
+ args,
31
+ env=env,
32
+ cwd=cwd,
33
+ capture_output=True,
34
+ text=True,
35
+ timeout=120,
36
+ )
37
+ if result.returncode != 0:
38
+ raise HTTPException(
39
+ status_code=500,
40
+ detail=f"Command failed: {result.stderr.strip()}",
41
+ )
42
+ return result.stdout.strip()
43
+
44
+
45
+ @app.get("/health")
46
+ def health():
47
+ return {"status": "ok"}
48
+
49
+
50
+ @app.post("/store-auth")
51
+ async def store_auth(request: Request):
52
+ body = await request.json()
53
+ AUTH_FILE.write_text(json.dumps(body, indent=2))
54
+ global AUTH_JSON
55
+ AUTH_JSON = json.dumps(body)
56
+ return {"stored": True}
57
+
58
+
59
+ @app.post("/run")
60
+ def run(job: Job):
61
+ with tempfile.TemporaryDirectory() as td:
62
+ if job.action == "list":
63
+ out = run_cmd(["notebooklm", "list", "--json"], td)
64
+ return json.loads(out)
65
+
66
+ if job.action == "create":
67
+ title = job.title or "Untitled"
68
+ out = run_cmd(["notebooklm", "create", title, "--json"], td)
69
+ result = json.loads(out)
70
+
71
+ if job.sourceUrls:
72
+ notebook_id = result.get("id", result.get("notebook_id"))
73
+ if notebook_id:
74
+ run_cmd(["notebooklm", "use", notebook_id], td)
75
+ for url in job.sourceUrls:
76
+ run_cmd(["notebooklm", "source", "add", url], td)
77
+ out = run_cmd(["notebooklm", "source", "list", "--json"], td)
78
+ result["sources"] = json.loads(out)
79
+
80
+ return result
81
+
82
+ if job.action == "ask":
83
+ if not job.notebookId or not job.prompt:
84
+ raise HTTPException(
85
+ status_code=400,
86
+ detail="ask requires notebookId and prompt",
87
+ )
88
+ run_cmd(["notebooklm", "use", job.notebookId], td)
89
+ out = run_cmd(["notebooklm", "ask", job.prompt, "--json"], td)
90
+ return json.loads(out)
91
+
92
+ if job.action == "summarize":
93
+ if not job.sourceUrls or not job.prompt:
94
+ raise HTTPException(
95
+ status_code=400,
96
+ detail="summarize requires sourceUrls and prompt",
97
+ )
98
+ title = job.title or "Summary Job"
99
+ out = run_cmd(["notebooklm", "create", title, "--json"], td)
100
+ result = json.loads(out)
101
+ notebook_id = result.get("id", result.get("notebook_id"))
102
+
103
+ if notebook_id:
104
+ run_cmd(["notebooklm", "use", notebook_id], td)
105
+ for url in job.sourceUrls:
106
+ run_cmd(["notebooklm", "source", "add", url], td)
107
+ out = run_cmd(["notebooklm", "ask", job.prompt, "--json"], td)
108
+ return json.loads(out)
109
+
110
+ raise HTTPException(status_code=500, detail="Failed to create notebook")
111
+
112
+ if job.action == "delete":
113
+ if not job.notebookId:
114
+ raise HTTPException(
115
+ status_code=400,
116
+ detail="delete requires notebookId",
117
+ )
118
+ out = run_cmd(
119
+ ["notebooklm", "delete", job.notebookId, "--json"], td
120
+ )
121
+ return json.loads(out) if out else {"deleted": True}
122
+
123
+ raise HTTPException(status_code=400, detail=f"Unknown action: {job.action}")
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "notebooklm-api-client",
3
+ "version": "0.1.0",
4
+ "description": "NotebookLM API via Cloudflare Containers — on-demand Python sandbox that sleeps when idle",
5
+ "author": "vtempest <grokthiscontact@gmail.com>",
6
+ "license": "AGPL-3.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/OpenSourceAGI/qwksearch-research-agent.git",
10
+ "directory": "packages/notebooklm-api"
11
+ },
12
+ "type": "module",
13
+ "main": "./src/index.ts",
14
+ "scripts": {
15
+ "dev": "wrangler dev",
16
+ "deploy": "wrangler deploy",
17
+ "types": "wrangler types"
18
+ },
19
+ "dependencies": {
20
+ "@cloudflare/puppeteer": "^1.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@cloudflare/workers-types": "^4.20241127.0",
24
+ "typescript": "^5.9.3",
25
+ "wrangler": "^4.0.0"
26
+ }
27
+ }
package/readme.md ADDED
@@ -0,0 +1,98 @@
1
+ # notebooklm-api
2
+
3
+ NotebookLM API powered by Cloudflare Containers. The Python sandbox runs on demand and sleeps after 5 minutes of inactivity to conserve costs.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Request → Worker (auth + routing) → Container (notebooklm-py) → Response
9
+ ↕
10
+ sleeps when idle
11
+ ```
12
+
13
+ - **Worker** validates auth and routes requests to the container
14
+ - **Container** runs `notebooklm-py` CLI in a Python 3.12 sandbox
15
+ - Container auto-sleeps after `sleepAfter` (5m) — billing stops until next request
16
+ - Container wakes transparently on next incoming request
17
+
18
+ ## API
19
+
20
+ All endpoints require `Authorization: Bearer <API_TOKEN>` header.
21
+
22
+ ### POST /
23
+
24
+ ```json
25
+ { "action": "list" }
26
+ ```
27
+
28
+ ```json
29
+ { "action": "create", "title": "My Notebook", "sourceUrls": ["https://..."] }
30
+ ```
31
+
32
+ ```json
33
+ { "action": "ask", "notebookId": "abc123", "prompt": "Summarize the key points" }
34
+ ```
35
+
36
+ ```json
37
+ { "action": "summarize", "sourceUrls": ["https://..."], "prompt": "What are the main findings?" }
38
+ ```
39
+
40
+ ```json
41
+ { "action": "delete", "notebookId": "abc123" }
42
+ ```
43
+
44
+ ## Login via Browser Automation
45
+
46
+ The Worker uses Cloudflare Browser Rendering (Puppeteer) to automate Google login — no manual cookie export needed.
47
+
48
+ ```bash
49
+ # First call — enters email + password, triggers 2FA
50
+ curl -X POST https://notebooklm-api.<you>.workers.dev \
51
+ -H "Authorization: Bearer $API_TOKEN" \
52
+ -H "Content-Type: application/json" \
53
+ -d '{"action": "login"}'
54
+
55
+ # If 2FA is required — pass the security code
56
+ curl -X POST https://notebooklm-api.<you>.workers.dev \
57
+ -H "Authorization: Bearer $API_TOKEN" \
58
+ -H "Content-Type: application/json" \
59
+ -d '{"action": "login", "securityCode": "123456"}'
60
+ ```
61
+
62
+ Auth cookies are stored in the container's Durable Object storage and injected on container start.
63
+
64
+ ## Setup
65
+
66
+ 1. Set secrets:
67
+ ```bash
68
+ wrangler secret put API_TOKEN
69
+ wrangler secret put GOOGLE_EMAIL
70
+ wrangler secret put GOOGLE_PASSWORD
71
+ ```
72
+
73
+ 2. Deploy:
74
+ ```bash
75
+ cd packages/notebooklm-api
76
+ wrangler deploy
77
+ ```
78
+
79
+ 3. Trigger login:
80
+ ```bash
81
+ curl -X POST https://notebooklm-api.<you>.workers.dev \
82
+ -H "Authorization: Bearer $TOKEN" \
83
+ -d '{"action": "login"}'
84
+ ```
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ bun install
90
+ wrangler dev
91
+ ```
92
+
93
+ ## Cost
94
+
95
+ - Container only bills while awake (per 10ms granularity)
96
+ - `basic` instance: 1/4 vCPU, 1 GiB RAM — ~$0.000005/sec when active
97
+ - Idle = $0
98
+ - `max_instances = 3` caps concurrent cost
package/src/index.ts ADDED
@@ -0,0 +1,195 @@
1
+ import { Container } from "cloudflare:workers";
2
+ import puppeteer from "@cloudflare/puppeteer";
3
+
4
+ export interface Env {
5
+ NOTEBOOK_RUNNER: DurableObjectNamespace<NotebookRunner>;
6
+ BROWSER: Fetcher;
7
+ API_TOKEN: string;
8
+ GOOGLE_EMAIL: string;
9
+ GOOGLE_PASSWORD: string;
10
+ }
11
+
12
+ export interface NotebookJob {
13
+ action: "create" | "ask" | "summarize" | "list" | "delete" | "login";
14
+ notebookId?: string;
15
+ sourceUrls?: string[];
16
+ title?: string;
17
+ prompt?: string;
18
+ securityCode?: string;
19
+ }
20
+
21
+ export class NotebookRunner extends Container {
22
+ defaultPort = 8080;
23
+ sleepAfter = "5m";
24
+
25
+ override async onStart() {
26
+ this.ctx.container.start({
27
+ env: {
28
+ NOTEBOOKLM_AUTH_JSON: (await this.ctx.storage.get<string>("auth")) ?? "",
29
+ },
30
+ enableInternet: true,
31
+ });
32
+ }
33
+ }
34
+
35
+ export default {
36
+ async fetch(request: Request, env: Env): Promise<Response> {
37
+ if (request.method === "OPTIONS") {
38
+ return new Response(null, {
39
+ headers: {
40
+ "access-control-allow-origin": "*",
41
+ "access-control-allow-methods": "POST, OPTIONS",
42
+ "access-control-allow-headers": "content-type, authorization",
43
+ },
44
+ });
45
+ }
46
+
47
+ if (request.method !== "POST") {
48
+ return json({ error: "Method not allowed" }, 405);
49
+ }
50
+
51
+ const authHeader = request.headers.get("authorization");
52
+ if (authHeader !== `Bearer ${env.API_TOKEN}`) {
53
+ return json({ error: "Unauthorized" }, 401);
54
+ }
55
+
56
+ let body: NotebookJob;
57
+ try {
58
+ body = await request.json();
59
+ } catch {
60
+ return json({ error: "Invalid JSON body" }, 400);
61
+ }
62
+
63
+ if (!body.action) {
64
+ return json({ error: "Missing action field" }, 400);
65
+ }
66
+
67
+ if (body.action === "login") {
68
+ return handleLogin(env, body.securityCode);
69
+ }
70
+
71
+ const id = env.NOTEBOOK_RUNNER.idFromName("default");
72
+ const stub = env.NOTEBOOK_RUNNER.get(id);
73
+
74
+ const containerResp = await stub.fetch("http://container/run", {
75
+ method: "POST",
76
+ headers: { "content-type": "application/json" },
77
+ body: JSON.stringify(body),
78
+ });
79
+
80
+ const result = await containerResp.text();
81
+ return new Response(result, {
82
+ status: containerResp.status,
83
+ headers: {
84
+ "content-type": "application/json",
85
+ "access-control-allow-origin": "*",
86
+ },
87
+ });
88
+ },
89
+ };
90
+
91
+ async function handleLogin(env: Env, securityCode?: string): Promise<Response> {
92
+ const browser = await puppeteer.launch(env.BROWSER);
93
+
94
+ try {
95
+ const page = await browser.newPage();
96
+ await page.setUserAgent(
97
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
98
+ );
99
+
100
+ await page.goto("https://notebooklm.google.com", {
101
+ waitUntil: "networkidle0",
102
+ });
103
+
104
+ // Google sign-in: enter email
105
+ await page.waitForSelector('input[type="email"]', { timeout: 15000 });
106
+ await page.type('input[type="email"]', env.GOOGLE_EMAIL, { delay: 50 });
107
+ await page.click("#identifierNext");
108
+ await page.waitForNavigation({ waitUntil: "networkidle0" }).catch(() => {});
109
+
110
+ // Enter password
111
+ await page.waitForSelector('input[type="password"]', {
112
+ visible: true,
113
+ timeout: 15000,
114
+ });
115
+ await delay(1000);
116
+ await page.type('input[type="password"]', env.GOOGLE_PASSWORD, {
117
+ delay: 50,
118
+ });
119
+ await page.click("#passwordNext");
120
+ await page.waitForNavigation({ waitUntil: "networkidle0" }).catch(() => {});
121
+
122
+ // Handle security code / 2FA if provided
123
+ if (securityCode) {
124
+ await delay(2000);
125
+ const codeInput = await page.$(
126
+ 'input[type="tel"], input[name="totpPin"], input[id="idvPin"]'
127
+ );
128
+ if (codeInput) {
129
+ await codeInput.type(securityCode, { delay: 50 });
130
+ const nextBtn = await page.$(
131
+ 'button[data-idom-class*="next"], #idvPreregisteredPhoneNext, button[jsname="LgbsSe"]'
132
+ );
133
+ if (nextBtn) await nextBtn.click();
134
+ await page
135
+ .waitForNavigation({ waitUntil: "networkidle0", timeout: 15000 })
136
+ .catch(() => {});
137
+ }
138
+ }
139
+
140
+ // Wait for NotebookLM to load (confirms successful login)
141
+ await page.waitForSelector("body", { timeout: 10000 });
142
+ await delay(3000);
143
+
144
+ // Extract cookies and local storage auth
145
+ const cookies = await page.cookies();
146
+ const localStorage = await page.evaluate(() => {
147
+ const data: Record<string, string> = {};
148
+ for (let i = 0; i < window.localStorage.length; i++) {
149
+ const key = window.localStorage.key(i)!;
150
+ data[key] = window.localStorage.getItem(key)!;
151
+ }
152
+ return data;
153
+ });
154
+
155
+ const authData = JSON.stringify({
156
+ cookies,
157
+ localStorage,
158
+ timestamp: new Date().toISOString(),
159
+ });
160
+
161
+ // Store auth in the container's Durable Object storage
162
+ const id = env.NOTEBOOK_RUNNER.idFromName("default");
163
+ const stub = env.NOTEBOOK_RUNNER.get(id);
164
+ await stub.fetch("http://container/store-auth", {
165
+ method: "POST",
166
+ headers: { "content-type": "application/json" },
167
+ body: authData,
168
+ });
169
+
170
+ return json({
171
+ success: true,
172
+ message: "Login complete, auth stored for container",
173
+ cookieCount: cookies.length,
174
+ });
175
+ } catch (err: unknown) {
176
+ const message = err instanceof Error ? err.message : "Unknown error";
177
+ return json({ error: "Login failed", detail: message }, 500);
178
+ } finally {
179
+ await browser.close();
180
+ }
181
+ }
182
+
183
+ function delay(ms: number): Promise<void> {
184
+ return new Promise((resolve) => setTimeout(resolve, ms));
185
+ }
186
+
187
+ function json(data: unknown, status = 200): Response {
188
+ return new Response(JSON.stringify(data), {
189
+ status,
190
+ headers: {
191
+ "content-type": "application/json",
192
+ "access-control-allow-origin": "*",
193
+ },
194
+ });
195
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ESNext"],
7
+ "types": ["@cloudflare/workers-types"],
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }