-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscrape-notebooklm-workflow.js
More file actions
328 lines (284 loc) · 11.8 KB
/
Copy pathscrape-notebooklm-workflow.js
File metadata and controls
328 lines (284 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/**
* BrainBrief - NotebookLM Workflow Scraper
*
* Purpose: Scrape UI selectors for each stage of NotebookLM workflow
* Usage: node scrape-notebooklm-workflow.js
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const logger = require('./src/utils/logger');
const readline = require('readline');
const BROWSER_DATA_DIR = path.join(__dirname, 'browser-data/notebooklm');
const NOTEBOOKLM_URL = 'https://notebooklm.google.com';
const OUTPUT_DIR = path.join(__dirname, 'notebooklm-scraped');
// Create readline interface for user prompts
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function prompt(question) {
return new Promise(resolve => {
rl.question(question, answer => resolve(answer));
});
}
/**
* Scrape current page UI
*/
async function scrapePage(page, stageName) {
logger.info(`Scraping ${stageName}...`, 'scraper');
const uiData = await page.evaluate(() => {
const results = {
url: window.location.href,
title: document.title,
buttons: [],
inputs: [],
textElements: [],
ariaElements: []
};
// All buttons
document.querySelectorAll('button').forEach((btn, idx) => {
if (btn.offsetParent !== null) { // Only visible
results.buttons.push({
index: idx,
text: btn.innerText?.trim(),
ariaLabel: btn.getAttribute('aria-label'),
ariaDescribedBy: btn.getAttribute('aria-describedby'),
className: btn.className,
id: btn.id,
type: btn.type,
disabled: btn.disabled
});
}
});
// All inputs
document.querySelectorAll('input').forEach((input, idx) => {
results.inputs.push({
index: idx,
type: input.type,
accept: input.accept,
ariaLabel: input.getAttribute('aria-label'),
placeholder: input.placeholder,
className: input.className,
id: input.id,
name: input.name,
visible: input.offsetParent !== null
});
});
// Elements with aria-labels (most reliable)
document.querySelectorAll('[aria-label]').forEach((el, idx) => {
if (el.offsetParent !== null) {
results.ariaElements.push({
index: idx,
tagName: el.tagName,
ariaLabel: el.getAttribute('aria-label'),
text: el.innerText?.trim().substring(0, 100),
className: el.className,
id: el.id
});
}
});
// Search for key terms
const keywords = ['upload', 'add', 'source', 'file', 'create', 'notebook', 'processing', 'import'];
keywords.forEach(keyword => {
const elements = Array.from(document.querySelectorAll('*'))
.filter(el => {
const text = el.innerText?.toLowerCase() || '';
const aria = el.getAttribute('aria-label')?.toLowerCase() || '';
return (text.includes(keyword) || aria.includes(keyword)) &&
el.offsetParent !== null &&
el.children.length === 0; // Leaf nodes only
})
.slice(0, 5); // Max 5 per keyword
elements.forEach(el => {
results.textElements.push({
keyword: keyword,
tagName: el.tagName,
text: el.innerText?.trim(),
ariaLabel: el.getAttribute('aria-label'),
className: el.className,
id: el.id
});
});
});
return results;
});
// Save stage data
const filename = `${stageName.toLowerCase().replace(/\s+/g, '-')}.json`;
const filepath = path.join(OUTPUT_DIR, filename);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
fs.writeFileSync(filepath, JSON.stringify(uiData, null, 2));
logger.success(`Saved ${stageName} UI data: ${filename}`, 'scraper');
logger.info(` Buttons: ${uiData.buttons.length}`, 'scraper');
logger.info(` Inputs: ${uiData.inputs.length}`, 'scraper');
logger.info(` Aria elements: ${uiData.ariaElements.length}`, 'scraper');
logger.info(` Text elements: ${uiData.textElements.length}`, 'scraper');
return uiData;
}
/**
* Main workflow scraper
*/
async function scrapeWorkflow() {
logger.info('='.repeat(70));
logger.info('NOTEBOOKLM WORKFLOW SCRAPER');
logger.info('='.repeat(70));
logger.info('');
logger.info('This will scrape UI selectors for each stage:', 'scraper');
logger.info(' 1. Landing page (notebook list)', 'scraper');
logger.info(' 2. Inside notebook (add sources)', 'scraper');
logger.info(' 3. Upload modal (file picker)', 'scraper');
logger.info(' 4. Processing state (after upload)', 'scraper');
logger.info('');
let context;
try {
context = await chromium.launchPersistentContext(BROWSER_DATA_DIR, {
headless: false,
viewport: { width: 1400, height: 1000 },
args: ['--disable-blink-features=AutomationControlled']
});
const page = await context.newPage();
// Stage 1: Landing page
logger.info('');
logger.info('='.repeat(70));
logger.info('STAGE 1: LANDING PAGE (Notebook List)');
logger.info('='.repeat(70));
logger.info('');
logger.info('What you should see:', 'scraper');
logger.info(' ✓ NotebookLM home page loaded', 'scraper');
logger.info(' ✓ List of your notebooks (or "Create new notebook" card)', 'scraper');
logger.info(' ✓ No notebook is open yet', 'scraper');
logger.info('');
await page.goto(NOTEBOOKLM_URL);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000);
logger.success('Browser loaded NotebookLM', 'scraper');
logger.info('');
logger.warn('ACTION REQUIRED:', 'scraper');
logger.info(' 1. Log in to Google if prompted', 'scraper');
logger.info(' 2. Wait for notebook list to appear', 'scraper');
logger.info(' 3. DO NOT open any notebook yet', 'scraper');
logger.info(' 4. Press Enter when you see the notebook list/grid', 'scraper');
logger.info('');
await prompt('>>> Press Enter when Stage 1 is ready (notebook list visible): ');
const landingData = await scrapePage(page, 'stage-1-landing');
logger.success('✅ Stage 1 scraped!', 'scraper');
// Stage 2: Inside notebook
logger.info('');
logger.info('='.repeat(70));
logger.info('STAGE 2: INSIDE NOTEBOOK (Sources Page)');
logger.info('='.repeat(70));
logger.info('');
logger.info('What you should see:', 'scraper');
logger.info(' ✓ Notebook is open (title at top)', 'scraper');
logger.info(' ✓ Sources section visible', 'scraper');
logger.info(' ✓ "Add sources" or "Upload" button somewhere', 'scraper');
logger.info(' ✓ DO NOT click anything yet', 'scraper');
logger.info('');
logger.warn('ACTION REQUIRED:', 'scraper');
logger.info(' 1. Click to open an existing notebook', 'scraper');
logger.info(' OR create a new notebook (name it "BrainBrief Test")', 'scraper');
logger.info(' 2. Wait for notebook to fully load', 'scraper');
logger.info(' 3. Look for "Add sources" or upload area', 'scraper');
logger.info(' 4. DO NOT click upload yet', 'scraper');
logger.info(' 5. Press Enter when you see the sources/upload area', 'scraper');
logger.info('');
await prompt('>>> Press Enter when Stage 2 is ready (inside notebook, upload button visible): ');
await page.waitForTimeout(2000);
const notebookData = await scrapePage(page, 'stage-2-notebook');
logger.success('✅ Stage 2 scraped!', 'scraper');
// Stage 3: Upload modal
logger.info('');
logger.info('='.repeat(70));
logger.info('STAGE 3: UPLOAD MODAL/DIALOG');
logger.info('='.repeat(70));
logger.info('');
logger.info('What you should see:', 'scraper');
logger.info(' ✓ Upload dialog or modal is open', 'scraper');
logger.info(' ✓ File picker OR drag-drop area', 'scraper');
logger.info(' ✓ Options: "Upload from computer", "From Google Drive", etc.', 'scraper');
logger.info(' ✓ DO NOT upload a file yet', 'scraper');
logger.info('');
logger.warn('ACTION REQUIRED:', 'scraper');
logger.info(' 1. Click the "Add sources" or "Upload" button', 'scraper');
logger.info(' 2. Wait for upload dialog/modal to appear', 'scraper');
logger.info(' 3. DO NOT select a file yet', 'scraper');
logger.info(' 4. Press Enter when dialog is fully visible', 'scraper');
logger.info('');
await prompt('>>> Press Enter when Stage 3 is ready (upload dialog visible): ');
await page.waitForTimeout(1000);
const uploadModalData = await scrapePage(page, 'stage-3-upload-modal');
logger.success('✅ Stage 3 scraped!', 'scraper');
// Stage 4: Processing (optional)
logger.info('');
logger.info('='.repeat(70));
logger.info('STAGE 4: PROCESSING (Optional but Recommended)');
logger.info('='.repeat(70));
logger.info('');
const doProcessing = await prompt('Upload a test file to capture processing indicators? (y/n): ');
if (doProcessing.toLowerCase() === 'y') {
logger.info('');
logger.info('What you should see after uploading:', 'scraper');
logger.info(' ✓ File uploading...', 'scraper');
logger.info(' ✓ "Processing..." message or spinner', 'scraper');
logger.info(' ✓ Progress indicator', 'scraper');
logger.info('');
logger.warn('ACTION REQUIRED:', 'scraper');
logger.info(' 1. Select and upload a small text file (any .txt)', 'scraper');
logger.info(' 2. Watch for "Processing..." indicator', 'scraper');
logger.info(' 3. Press Enter when you see processing/loading', 'scraper');
logger.info('');
await prompt('>>> Press Enter when Stage 4 is ready (file processing): ');
await page.waitForTimeout(1000);
const processingData = await scrapePage(page, 'stage-4-processing');
logger.success('✅ Stage 4 scraped!', 'scraper');
// Stage 5: Complete
logger.info('');
logger.info('='.repeat(70));
logger.info('STAGE 5: UPLOAD COMPLETE');
logger.info('='.repeat(70));
logger.info('');
logger.info('What you should see:', 'scraper');
logger.info(' ✓ File appears in sources list', 'scraper');
logger.info(' ✓ "Ready" or checkmark indicator', 'scraper');
logger.info(' ✓ No more "Processing..." message', 'scraper');
logger.info('');
logger.warn('ACTION REQUIRED:', 'scraper');
logger.info(' 1. Wait for upload to complete', 'scraper');
logger.info(' 2. Verify file appears in sources', 'scraper');
logger.info(' 3. Press Enter when complete', 'scraper');
logger.info('');
await prompt('>>> Press Enter when Stage 5 is ready (upload complete): ');
await page.waitForTimeout(1000);
const completeData = await scrapePage(page, 'stage-5-complete');
logger.success('✅ Stage 5 scraped!', 'scraper');
}
// Summary
logger.info('');
logger.info('='.repeat(70));
logger.success('✅ WORKFLOW SCRAPING COMPLETE');
logger.info('='.repeat(70));
logger.info('');
logger.info(`All UI data saved to: ${OUTPUT_DIR}/`, 'scraper');
logger.info('');
logger.info('Files created:', 'scraper');
const files = fs.readdirSync(OUTPUT_DIR);
files.forEach(file => {
logger.info(` - ${file}`, 'scraper');
});
logger.info('');
logger.info('NEXT STEPS:', 'scraper');
logger.info(' 1. Review JSON files for upload button selector', 'scraper');
logger.info(' 2. Find file input selector', 'scraper');
logger.info(' 3. Find processing indicator', 'scraper');
logger.info(' 4. I will build automation with those selectors', 'scraper');
rl.close();
await context.close();
} catch (error) {
logger.error('Workflow scraping failed', error, 'scraper');
rl.close();
if (context) await context.close();
}
}
scrapeWorkflow();