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 +7 -0
- package/Dockerfile +9 -0
- package/container/server.py +123 -0
- package/package.json +27 -0
- package/readme.md +98 -0
- package/src/index.ts +195 -0
- package/tsconfig.json +13 -0
package/.dockerignore
ADDED
package/Dockerfile
ADDED
|
@@ -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
|
+
}
|