// ==UserScript== // @name NotebookLM Source Export // @namespace https://github.com/marmoris-x/tampermonkey-scripts // @version 6.3 // @author marmoris-x // @description Export NotebookLM sources and chat as ZIP archives // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=https://notebooklm.google/ // @supportURL https://github.com/marmoris-x/tampermonkey-scripts/issues // @downloadURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/NotebookLM%20Source%20Export.user.js // @updateURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/NotebookLM%20Source%20Export.user.js // @match https://notebooklm.google.com/* // @sandbox JavaScript // @grant GM_addElement // @grant GM_download // @grant GM_notification // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; let _liCache = new WeakMap(); function getLiIndex(node) { let ancestor = node.parentElement; while (ancestor && !["ul", "ol"].includes(ancestor.tagName.toLowerCase())) { ancestor = ancestor.parentElement; } if (!ancestor || ancestor.tagName.toLowerCase() !== "ol") return -1; const cached = _liCache.get(node); if (cached && cached.ancestor === ancestor) return cached.index; const allLis = ancestor.querySelectorAll("li"); for (let i = 0, idx = 1; i < allLis.length; i++) { let a = allLis[i].parentElement; while (a && !["ul", "ol"].includes(a.tagName.toLowerCase())) a = a.parentElement; if (a === ancestor) { _liCache.set(allLis[i], { ancestor, index: idx }); idx++; } } return (_liCache.get(node) || {}).index || 1; } function resetLiCache() { _liCache = new WeakMap(); } function htmlToMarkdown(el) { function convert(node) { var _a, _b; if (node.nodeType === Node.TEXT_NODE) return node.textContent; if (node.nodeType !== Node.ELEMENT_NODE) return ""; const tag = node.tagName.toLowerCase(); function inner() { let result = ""; for (let child = node.firstChild; child; child = child.nextSibling) { result += convert(child); } return result; } switch (tag) { case "h1": return "# " + inner() + "\n\n"; case "h2": return "## " + inner() + "\n\n"; case "h3": return "### " + inner() + "\n\n"; case "h4": return "#### " + inner() + "\n\n"; case "h5": return "##### " + inner() + "\n\n"; case "h6": return "###### " + inner() + "\n\n"; case "p": return inner() + "\n\n"; case "br": return "\n"; case "strong": case "b": return "**" + inner() + "**"; case "em": case "i": return "*" + inner() + "*"; case "a": { let href = node.getAttribute("href") || ""; if (href.includes("google.com/url")) { try { href = new URL(href).searchParams.get("q") || href; } catch (_) { } } const text = inner(); return href ? "[" + text + "](" + href + ")" : text; } case "ul": return inner() + "\n"; case "ol": return inner() + "\n"; case "li": { const index = getLiIndex(node); if (index > 0) return index + ". " + inner().trim() + "\n"; return "- " + inner().trim() + "\n"; } case "div": { const ariaLevel = node.getAttribute("aria-level"); if (ariaLevel) { const hashes = "#".repeat(Math.min(parseInt(ariaLevel), 6)); return hashes + " " + inner().trim() + "\n\n"; } if (/^-{10,}$/.test(node.textContent.trim())) return "---\n\n"; if (node.classList && node.classList.contains("code")) { const codeEl = node.querySelector("code"); const lang = codeEl ? codeEl.getAttribute("data-language") || "" : ""; const content = codeEl ? codeEl.textContent : node.textContent; return "```" + lang + "\n" + content + "\n```\n\n"; } return inner(); } case "s": case "del": case "strike": return "~~" + inner() + "~~"; case "u": return "__" + inner() + "__"; case "code": { if (node.parentElement && node.parentElement.tagName.toLowerCase() === "pre") return inner(); return "`" + inner() + "`"; } case "pre": { const codeEl = node.querySelector("code"); const langSource = codeEl || node; const lang = langSource.getAttribute("data-language") || langSource.className && ((_a = langSource.className.match(/language-(\S+)/)) == null ? void 0 : _a[1]) || langSource.className && ((_b = langSource.className.match(/lang-(\S+)/)) == null ? void 0 : _b[1]) || ""; return "```" + lang + "\n" + (codeEl ? codeEl.textContent : inner()) + "\n```\n\n"; } case "blockquote": return inner().trim().split("\n").map(function(l) { return "> " + l; }).join("\n") + "\n\n"; case "hr": return "---\n\n"; case "img": { const src = node.getAttribute("src") || ""; const alt = node.getAttribute("alt") || ""; return "![" + alt + "](" + src + ")"; } case "table": { let toRow2 = function(cells) { return "| " + cells.map(function(c) { return c.textContent.trim().replace(/\|/g, "\\|"); }).join(" | ") + " |"; }; const rows = Array.from(node.querySelectorAll("tr")); if (!rows.length) return inner(); const headerCells = Array.from(rows[0].querySelectorAll("th, td")); const header = toRow2(headerCells); const separator = "| " + headerCells.map(function() { return "---"; }).join(" | ") + " |"; const body = rows.slice(1).map(function(r) { return toRow2(Array.from(r.querySelectorAll("td"))); }).join("\n"); return [header, separator, body].filter(Boolean).join("\n") + "\n\n"; } default: return inner(); } } return convert(el).replace(/\n{3,}/g, "\n\n").trim(); } const crcTable = new Uint32Array(256); (function buildCRCTable() { for (let i = 0; i < 256; i++) { let c = i; for (let j = 0; j < 8; j++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1; crcTable[i] = c; } })(); function crc32(u8) { let crc = 4294967295; for (let i = 0; i < u8.length; i++) crc = crc >>> 8 ^ crcTable[(crc ^ u8[i]) & 255]; return (crc ^ 4294967295) >>> 0; } function u16(n) { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, n, true); return b; } function u32(n) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; } const enc = new TextEncoder(); function buildStoreZip(files) { const localParts = []; const centralParts = []; const entries = []; let localOffset = 0; for (let f = 0; f < files.length; f++) { const nameBytes = enc.encode(files[f].name); const data = files[f].data || enc.encode(files[f].text); const crc = crc32(data); const local = new Uint8Array([ 80, 75, 3, 4, 20, 0, ...u16(2048), 0, 0, 0, 0, 0, 0, ...u32(crc), ...u32(data.length), ...u32(data.length), ...u16(nameBytes.length), 0, 0 ]); entries.push({ offset: localOffset, nameBytes, crc, size: data.length }); localParts.push(local, nameBytes, data); localOffset += local.length + nameBytes.length + data.length; } let centralSize = 0; for (let f = 0; f < entries.length; f++) { const e = entries[f]; const ch = new Uint8Array([ 80, 75, 1, 2, 20, 0, 20, 0, ...u16(2048), 0, 0, 0, 0, 0, 0, ...u32(e.crc), ...u32(e.size), ...u32(e.size), ...u16(e.nameBytes.length), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...u32(e.offset) ]); centralParts.push(ch, e.nameBytes); centralSize += ch.length + e.nameBytes.length; } const eocd = new Uint8Array([ 80, 75, 5, 6, 0, 0, 0, 0, ...u16(entries.length), ...u16(entries.length), ...u32(centralSize), ...u32(localOffset), 0, 0 ]); return new Blob([...localParts, ...centralParts, eocd], { type: "application/zip" }); } function createLogger(prefix, debugMode) { debugMode = debugMode || false; const tag = "[" + prefix + "]"; return { log: function() { const args = [tag]; for (let i = 0; i < arguments.length; i++) args.push(arguments[i]); console.log.apply(console, args); }, warn: function() { const args = [tag]; for (let i = 0; i < arguments.length; i++) args.push(arguments[i]); console.warn.apply(console, args); }, error: function() { const args = [tag]; for (let i = 0; i < arguments.length; i++) args.push(arguments[i]); console.error.apply(console, args); }, info: function() { const args = [tag]; for (let i = 0; i < arguments.length; i++) args.push(arguments[i]); console.info.apply(console, args); }, debug: function() { if (debugMode) { const args = [tag]; for (let i = 0; i < arguments.length; i++) args.push(arguments[i]); console.debug.apply(console, args); } } }; } const SELECTORS = { chatContainer: ".chat-panel-content", messagePair: ".chat-message-pair", userContent: ".from-user-container .message-text-content", aiContent: ".to-user-container labs-tailwind-doc-viewer", citationButton: ".citation-marker", notebookTitle: ".cover-title.mat-headline-medium", notebookDate: ".cover-subtitle-date", sourceCount: ".cover-subtitle-source-count" }; const FALLBACK_SELECTORS = { userContent: '[class*="from-user"] [class*="message-text"]', aiContent: '[class*="to-user"] [class*="message-text"], [class*="to-user"] labs-tailwind-doc-viewer', notebookTitle: '[class*="title"] [class*="mat-title"], [class*="cover-title"]', notebookDate: '[class*="subtitle"] [class*="date"], .cover-subtitle-date', sourceCount: '[class*="subtitle"] [class*="source"], .cover-subtitle-source-count' }; function queryFirst(root, selectors) { for (let i = 0; i < selectors.length; i++) { const el = root.querySelector(selectors[i]); if (el) return el; } return null; } function todayISO() { const d = new Date(); const pad = (n) => String(n).padStart(2, "0"); return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()); } function removeCitations(el) { if (!el) return; const buttons = el.querySelectorAll(SELECTORS.citationButton); for (let i = 0; i < buttons.length; i++) { buttons[i].remove(); } } function extractMetadata() { let titleEl = document.querySelector(SELECTORS.notebookTitle); if (!titleEl) { titleEl = queryFirst(document, [FALLBACK_SELECTORS.notebookTitle]); } const title = titleEl ? titleEl.textContent.trim() : "NotebookLM Chat"; let dateEl = document.querySelector(SELECTORS.notebookDate); if (!dateEl) { dateEl = queryFirst(document, [FALLBACK_SELECTORS.notebookDate]); } const dateStr = dateEl ? dateEl.textContent.trim() : todayISO(); let sourceEl = document.querySelector(SELECTORS.sourceCount); if (!sourceEl) { sourceEl = queryFirst(document, [FALLBACK_SELECTORS.sourceCount]); } const sourceInfo = sourceEl ? sourceEl.textContent.trim() : null; return { title, dateStr, sourceInfo }; } function extractChatToMarkdown(htmlToMarkdown2) { const container = document.querySelector(SELECTORS.chatContainer); if (!container) return ""; const pairs = container.querySelectorAll(SELECTORS.messagePair); if (!pairs || pairs.length === 0) return ""; const meta = extractMetadata(); const lines = [ "---", 'title: "' + meta.title + '"', "date: " + meta.dateStr, "platform: NotebookLM" ]; if (meta.sourceInfo) { lines.push("sources: " + meta.sourceInfo); } lines.push("---"); lines.push(""); lines.push("# NotebookLM Chat Export"); lines.push(""); for (let i = 0; i < pairs.length; i++) { const pair = pairs[i]; let userEl = pair.querySelector(SELECTORS.userContent); if (!userEl) { userEl = queryFirst(pair, [FALLBACK_SELECTORS.userContent]); } let aiEl = pair.querySelector(SELECTORS.aiContent); if (!aiEl) { aiEl = queryFirst(pair, [FALLBACK_SELECTORS.aiContent]); } const hasUserText = userEl && userEl.textContent.trim().length > 0; const hasAiResponse = aiEl && aiEl.textContent.trim().length > 0; if (!hasAiResponse) continue; lines.push("---"); lines.push(""); lines.push("## User"); lines.push(""); if (hasUserText) { lines.push(htmlToMarkdown2(userEl)); } else { lines.push("*[non-text message]*"); } lines.push(""); lines.push("## NotebookLM"); lines.push(""); removeCitations(aiEl); lines.push(htmlToMarkdown2(aiEl)); lines.push(""); } return lines.join("\n"); } function extractChatToText() { const container = document.querySelector(SELECTORS.chatContainer); if (!container) return ""; const pairs = container.querySelectorAll(SELECTORS.messagePair); if (!pairs || pairs.length === 0) return ""; const meta = extractMetadata(); const lines = [ "NotebookLM Chat Export", "Title: " + meta.title, "Date: " + meta.dateStr, "Platform: NotebookLM" ]; if (meta.sourceInfo) lines.push("Sources: " + meta.sourceInfo); lines.push(""); lines.push("---"); lines.push(""); for (let i = 0; i < pairs.length; i++) { const pair = pairs[i]; let userEl = pair.querySelector(SELECTORS.userContent); if (!userEl) userEl = queryFirst(pair, [FALLBACK_SELECTORS.userContent]); let aiEl = pair.querySelector(SELECTORS.aiContent); if (!aiEl) aiEl = queryFirst(pair, [FALLBACK_SELECTORS.aiContent]); const hasAiResponse = aiEl && aiEl.textContent.trim().length > 0; if (!hasAiResponse) continue; lines.push("User:"); if (userEl && userEl.textContent.trim().length > 0) { lines.push(userEl.textContent.trim()); } else { lines.push("[non-text message]"); } lines.push(""); lines.push("NotebookLM:"); const aiClone = aiEl.cloneNode(true); removeCitations(aiClone); lines.push(aiClone.textContent.trim()); lines.push(""); lines.push("---"); lines.push(""); } return lines.join("\n"); } function extractChatMessages() { const container = document.querySelector(SELECTORS.chatContainer); if (!container) return null; const pairs = container.querySelectorAll(SELECTORS.messagePair); if (!pairs || pairs.length === 0) return null; const meta = extractMetadata(); const messages = []; for (let i = 0; i < pairs.length; i++) { const pair = pairs[i]; let userEl = pair.querySelector(SELECTORS.userContent); if (!userEl) { userEl = queryFirst(pair, [FALLBACK_SELECTORS.userContent]); } let aiEl = pair.querySelector(SELECTORS.aiContent); if (!aiEl) { aiEl = queryFirst(pair, [FALLBACK_SELECTORS.aiContent]); } const hasUserText = userEl && userEl.textContent.trim().length > 0; const hasAiResponse = aiEl && aiEl.textContent.trim().length > 0; if (!hasAiResponse) continue; let userHtml = null; if (userEl && hasUserText) { const userClone = userEl.cloneNode(true); userHtml = userClone.innerHTML; } let aiHtml = null; if (aiEl) { const aiClone = aiEl.cloneNode(true); removeCitations(aiClone); aiHtml = aiClone.innerHTML; } messages.push({ userHtml, aiHtml, userText: userEl ? userEl.textContent.trim() : "", aiText: aiEl ? aiEl.textContent.trim() : "" }); } if (messages.length === 0) return null; return { notebookTitle: meta.title, dateStr: meta.dateStr, sourceInfo: meta.sourceInfo, messages }; } const EXPORT_CSS = [ "*, *::before, *::after { box-sizing:border-box; margin:0; padding:0; }", 'body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;', " font-size:16px; line-height:1.6; color:#1e293b; background:#fff; padding:40px 24px; max-width:800px; margin:0 auto; }", "h1 { font-size:24px; font-weight:600; color:#0f172a; margin-bottom:4px; }", "h2 { font-size:17px; font-weight:600; color:#334155; margin-bottom:10px; }", "a { color:#6366f1; text-decoration:none; }", "a:hover { text-decoration:underline; }", 'code { font-family:"SF Mono","Fira Code","Fira Mono","Roboto Mono",monospace; font-size:0.9em;', " background:#f1f5f9; padding:2px 6px; border-radius:4px; }", "pre { background:#0f172a; color:#e2e8f0; padding:16px; border-radius:8px; overflow-x:auto;", " font-size:14px; line-height:1.5; margin:12px 0; }", "pre code { background:transparent; padding:0; color:inherit; }", "blockquote { border-left:3px solid #6366f1; padding:8px 16px; margin:12px 0; background:#f8fafc;", " border-radius:0 8px 8px 0; }", "blockquote p { margin:4px 0; }", "table { border-collapse:collapse; width:100%; margin:12px 0; font-size:14px; }", "th, td { border:1px solid #e2e8f0; padding:8px 12px; text-align:left; }", "th { background:#f8fafc; font-weight:600; color:#334155; }", "tr:nth-child(even) { background:#f8fafc; }", "ul, ol { padding-left:24px; margin:8px 0; }", "li { margin:4px 0; }", "p { margin:8px 0; }", "hr { border:none; border-top:1px solid #e2e8f0; margin:16px 0; }", "img { max-width:100%; height:auto; border-radius:6px; }", ".metadata { background:#f8fafc; border:1px solid #e2e8f0; border-radius:10px; padding:16px 20px; margin:20px 0; }", ".metadata h1 { font-size:20px; margin-bottom:8px; }", ".metadata dl { display:grid; grid-template-columns:auto 1fr; gap:4px 12px; font-size:14px; }", ".metadata dt { color:#64748b; font-weight:500; }", ".metadata dd { color:#1e293b; }", ".message { margin:24px 0; padding:16px 20px; border-radius:12px; }", ".user-message { background:#f1f5f9; border:1px solid #e2e8f0; }", ".ai-message { background:#faf5ff; border:1px solid #e8d5ff; }", ".message .label { font-size:14px; font-weight:600; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:8px; }", ".user-message .label { color:#6366f1; }", ".ai-message .label { color:#a855f7; }", ".non-text { color:#94a3b8; font-style:italic; }", "@media print { body { padding:20px; font-size:13px; } .message { break-inside:avoid; } }", "labs-tailwind-doc-viewer, paragraph-element-view { display:block; }", ".table-wrapper { overflow-x:auto; }" ].join("\n"); function sanitizeHTML(html) { if (!html) return ""; return html.replace(//gi, "").replace(/\bon\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/javascript\s*:/gi, ""); } function buildChatHTMLDocument(messagesData, options) { options = options || {}; const meta = messagesData; let bodyParts = ""; for (let i = 0; i < meta.messages.length; i++) { const msg = meta.messages[i]; bodyParts += '
\n'; bodyParts += '
User
\n'; if (msg.userHtml) { bodyParts += '
' + sanitizeHTML(msg.userHtml) + "
\n"; } else { bodyParts += '
[non-text message]
\n'; } bodyParts += "
\n\n"; bodyParts += '
\n'; bodyParts += '
NotebookLM
\n'; if (msg.aiHtml) { bodyParts += '
' + sanitizeHTML(msg.aiHtml) + "
\n"; } else { bodyParts += '
[empty response]
\n'; } bodyParts += "
\n\n"; } const printScript = options.forPrint ? "