Feature/western bandicoot - #1
Conversation
…ad functionality. Implemented OAuth token handling in the service worker, enabling direct uploads to Google Drive and Gemini from the content script. Updated README to clarify the new architecture and functionality, including large uploads and background processing. Adjusted permissions in manifest.json to support new features.
…mented a recovery banner to restore unsaved sessions from IndexedDB, allowing users to save or download recordings after a crash. Updated CSS for the recovery banner styling and modified content.js to handle recovery logic. Enhanced README to document the new recovery functionality.
…n background.js and gdrive.js. Modify README to reflect the new folder structure for meeting recordings in Google Drive.
…oduced separate audio track recording alongside video, allowing for efficient audio uploads to Gemini. Updated data handling in IndexedDB to support audio chunks and modified upload logic to accommodate audio files. Incremented database version to v2 for improved chunk indexing. Updated README to reflect new audio features and changes in functionality.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughРозширення для запису Google Meet суттєво переписано: додано новий модуль ChangesMeet Recorder — повний рефакторинг
Sequence Diagram(s)sequenceDiagram
participant User
participant ContentScript
participant RecStore as RecStore (IndexedDB)
participant ServiceWorker
participant DriveAPI as Google Drive API
participant GeminiAPI as Gemini Files API
rect rgba(70, 130, 180, 0.5)
Note over ContentScript,RecStore: Запис
User->>ContentScript: startCapture()
ContentScript->>RecStore: startSession(meta)
ContentScript->>ContentScript: старт videoRecorder + audioRecorder (1s chunks)
loop ondataavailable
ContentScript->>RecStore: appendChunk(video) / appendChunk(audio)
end
end
rect rgba(60, 179, 113, 0.5)
Note over ContentScript,DriveAPI: Збереження
User->>ContentScript: stopCapture()
ContentScript->>RecStore: readBlob(video) + readBlob(audio)
ContentScript->>ServiceWorker: GET_TOKEN
ServiceWorker-->>ContentScript: OAuth token
ContentScript->>DriveAPI: uploadResumable(videoBlob) → fileId, folderId
ContentScript->>GeminiAPI: geminiUploadFile(audioBlob) → fileUri
ContentScript->>ServiceWorker: GEMINI_CONTINUE(job)
end
rect rgba(255, 165, 0, 0.5)
Note over ServiceWorker,DriveAPI: Фонова Gemini-обробка
loop chrome.alarms (кожну хвилину)
ServiceWorker->>GeminiAPI: geminiGetFile(fileUri)
alt ACTIVE
ServiceWorker->>GeminiAPI: geminiGenerate(fileUri) → текст
ServiceWorker->>DriveAPI: createDriveDoc(text)
ServiceWorker->>ServiceWorker: finishGeminiJob(done)
else PROCESSING
ServiceWorker->>ServiceWorker: ticks++
else FAILED / timeout
ServiceWorker->>ServiceWorker: finishGeminiJob(error)
end
end
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
❌ The last analysis has failed. |
PR Summary by QodoMove Drive/Gemini uploads to content script, add crash recovery + audio-only Gemini Description
Diagram
High-Level Assessment
Files changed (8)
|
Code Review by Qodo
1. Втрачений останній chunk
|
| recorder.ondataavailable = (e) => { | ||
| if (!e.data || !e.data.size) return; | ||
| if (recId) RecStore.appendChunk(recId, e.data, 'video').catch((err) => console.warn('[MeetRec] appendChunk:', err)); | ||
| else chunks.push(e.data); | ||
| }; |
There was a problem hiding this comment.
1. Втрачений останній chunk 🐞 Bug ≡ Correctness
У content.js chunk’и пишуться в IndexedDB через RecStore.appendChunk() без очікування завершення, але onRecorderStop() одразу збирає Blob через RecStore.readBlob(), тому останній(і) chunk(и) можуть не потрапити у фінальний файл. Після успішного збереження код видаляє сесію з IndexedDB, роблячи таку втрату незворотною.
Agent Prompt
### Issue description
`RecStore.appendChunk()` викликається асинхронно (і не очікується), а фіналізація (`readBlob()` + `deleteSession()`) може стартувати раніше, ніж закомічено останні транзакції запису chunk’ів. Це дає ризик тихої усіченості кінця запису.
### Issue Context
- `RecStore.appendChunk()` є `async` і завершується лише після `tx(...).oncomplete`.
- У `ondataavailable` проміси не трекаються, отже `onRecorderStop()` не має гарантії, що всі записи в IndexedDB завершені.
### Fix Focus Areas
- content.js[142-146]
- content.js[190-224]
- recstore.js[67-72]
### Suggested approach
1. Збирати проміси запису chunk’ів у масив/чергу (наприклад, `pendingWrites.push(RecStore.appendChunk(...))`).
2. У `onRecorderStop()` (перед `readBlob()` і тим паче перед `deleteSession()`) виконати `await Promise.allSettled(pendingWrites)` (окремо для відео й аудіо або спільно).
3. Аналогічно забезпечити flush для аудіо-доріжки (подія `audioRecorder.stop` не гарантує commit IndexedDB-записів без await).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| async function saveRecording(blob, name) { | ||
| try { | ||
| const { folderId } = await withToken((token) => GDrive.uploadResumable(token, blob, name)); | ||
| return { where: 'drive', folderId }; | ||
| } catch (driveErr) { | ||
| console.warn('[MeetRec] Drive недоступний, зберігаю локально:', driveErr); | ||
| downloadLocally(blob, name); | ||
| return { where: 'local', folderId: null }; | ||
| } |
There was a problem hiding this comment.
2. Неперевірений локальний фолбек 🐞 Bug ☼ Reliability
saveRecording() у фолбеку на локальне збереження одразу повертає {where:'local'}, не перевіряючи,
що downloadLocally() реально відпрацював без помилки. Далі onRecorderStop() може видалити сесію з
IndexedDB, тож у випадку збою локального завантаження журнал для відновлення вже втрачено.
Agent Prompt
### Issue description
Фолбек "локально" не має сигналу успіху/помилки, але потік поводиться так, ніби збереження гарантовано відбулося. Це може призвести до втрати можливості відновлення з IndexedDB.
### Issue Context
- `downloadLocally()` не повертає статус/проміс.
- `onRecorderStop()` видаляє сесію з IndexedDB після `saveRecording()`.
### Fix Focus Areas
- content.js[206-214]
- content.js[228-249]
### Suggested approach
- Обгорнути `downloadLocally()` в try/catch і при винятку кидати помилку (щоб сесію НЕ видаляти).
- Розглянути зміну логіки: видаляти сесію з IndexedDB лише після підтвердженої успішності хоча б одного каналу збереження (Drive або локального), або лишати сесію до явної дії користувача (як у recovery-банері).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Зібрати всі шматки однієї доріжки сесії в один Blob (за порядком вставки). | ||
| // Старі записи без поля kind трактуємо як 'video'. | ||
| async function readBlob(id, mime, kind = 'video') { | ||
| const parts = await tx('chunks', 'readonly', (t) => | ||
| reqDone(t.objectStore('chunks').index('rid').getAll(IDBKeyRange.only(id)))); | ||
| const wanted = (parts || []).filter((p) => (p.kind || 'video') === kind); | ||
| return new Blob(wanted.map((p) => p.blob), { type: mime || 'video/webm' }); | ||
| } |
There was a problem hiding this comment.
3. Readblob читає зайве 🐞 Bug ➹ Performance
RecStore.readBlob() завжди робить getAll() по rid і лише потім фільтрує за kind, тому при читанні відео в пам’ять підтягуються й аудіо-blob’и (і навпаки). На довгих записах це суттєво підвищує пікове споживання пам’яті під час фіналізації/відновлення.
Agent Prompt
### Issue description
`readBlob()` читає всі chunks сесії незалежно від доріжки, а потім відкидає непотрібні. Це зайві I/O та пам’ять.
### Issue Context
Є індекс `rid_kind`, але він використовується лише в `countChunks()`.
### Fix Focus Areas
- recstore.js[74-90]
### Suggested approach
- Для нових сесій читати через `index('rid_kind')` для потрібного `kind`.
- Для зворотної сумісності зі старими записами (без `kind`) можна:
- при `kind==='video'` додатково дочитувати legacy-записи (де `kind` відсутній) або
- міграцією/оновленням при першому читанні проставляти `kind` для існуючих chunks.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| case 'GEMINI_CONTINUE': | ||
| // content залив відео в Gemini → ведемо дрібну обробку у фоні (alarms). | ||
| startGeminiJob(msg.job) | ||
| .then(() => sendResponse({ ok: true })) | ||
| .catch((e) => sendResponse({ ok: false, error: e.message })); | ||
| return true; | ||
| } | ||
| }); | ||
|
|
||
| // Створити Google Doc із конспекту в тій самій теці (multipart → конвертація в Google Doc). | ||
| async function createDriveDoc(token, folderId, name, text) { | ||
| const boundary = 'meetrec_doc_boundary'; | ||
| const meta = JSON.stringify({ | ||
| name, | ||
| parents: [folderId], | ||
| mimeType: 'application/vnd.google-apps.document' | ||
| }); | ||
| const body = new Blob([ | ||
| `--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n`, | ||
| meta, | ||
| `\r\n--${boundary}\r\nContent-Type: text/markdown; charset=UTF-8\r\n\r\n`, | ||
| text, | ||
| `\r\n--${boundary}--` | ||
| ], { type: `multipart/related; boundary=${boundary}` }); | ||
| // ---- Фонова Gemini-обробка (переживає засинання SW через chrome.alarms) ---- | ||
| // job = { geminiFileName, fileUri, mimeType, docName, meetingBaseName, folderId } | ||
|
|
||
| const r = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', { | ||
| method: 'POST', | ||
| headers: { Authorization: `Bearer ${token}` }, | ||
| body | ||
| }); | ||
| if (!r.ok) throw httpError('doc create', r.status); | ||
| return r.json(); | ||
| async function startGeminiJob(job) { | ||
| await chrome.storage.local.set({ geminiJob: { ...job, ticks: 0 } }); | ||
| setStatus('Роблю конспект через Gemini…'); | ||
| await chrome.alarms.create(GEMINI_ALARM, { periodInMinutes: 0.5 }); |
There was a problem hiding this comment.
4. Gemini job перезаписується 🐞 Bug ☼ Reliability
Service worker зберігає лише один geminiJob у chrome.storage.local, і кожен GEMINI_CONTINUE перезаписує попередній. Оскільки обробка може тривати до ~30 хв, новий запис/конспект, запущений раніше завершення попереднього, може “витіснити” попередній job і залишити його без конспекту.
Agent Prompt
### Issue description
`geminiJob` — одиночний слот. Нові job’и замінюють старі, що призводить до втрати фонового конспекту при кількох записах підряд.
### Issue Context
- content script завжди відправляє `GEMINI_CONTINUE` після зупинки запису.
- SW тримає лише один job і один alarm.
### Fix Focus Areas
- background.js[103-119]
- background.js[129-163]
- content.js[251-273]
### Suggested approach
- Зберігати масив job’ів (`geminiJobs: []`) і обробляти послідовно (FIFO):
1) якщо черга порожня — стартувати alarm,
2) якщо не порожня — додати job і повернути успіх + статус "у черзі".
- Або явно відхиляти новий job, якщо вже є активний (і показати користувачу зрозумілий статус).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Pull request overview
This PR refactors the Meet recorder extension so recording is resilient to tab/browser crashes by persisting MediaRecorder chunks to IndexedDB, and it moves large uploads (Drive + Gemini file upload) into the content script to avoid passing large blobs through extension messaging.
Changes:
- Add an IndexedDB-backed recording journal (
recstore.js) and a recovery banner UI to resume unfinished recordings. - Introduce shared “pure” API helpers for Google Drive and Gemini (
gdrive.js,gemini.js) and adjust background/content responsibilities. - Update MV3 permissions and documentation to match the new flow (alarms-based background polling for Gemini).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| recstore.js | New IndexedDB journal for chunk persistence, session listing/pruning, and recovery support. |
| content.js | Writes chunks to RecStore, uploads directly to Drive, uploads media to Gemini, and adds recovery banner/actions. |
| background.js | Supplies OAuth tokens to content script and runs Gemini polling via chrome.alarms to survive SW sleep. |
| gdrive.js | New shared Drive helpers (folder creation, resumable upload, Google Doc creation). |
| gemini.js | New shared Gemini helpers (resumable file upload, status polling request, generateContent). |
| manifest.json | Adds alarms permission and loads new helper scripts before content.js. |
| content.css | Styles for the new “unfinished recording” recovery banner. |
| README.md | Updates architecture description and adds a resiliency section (needs alignment with actual Gemini media). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| setStatus(saved.where === 'drive' | ||
| ? 'Готово ✓ — збережено в Google Drive' | ||
| : 'Готово ✓ — збережено локально (тека «Завантаження»)'); | ||
| if (id) await RecStore.deleteSession(id); // відео в безпеці → журнал більше не потрібен | ||
| await maybeGemini(audioBlob, name, saved.folderId); // ставить власні статуси |
| const audioBlob = await RecStore.readBlob(session.id, 'audio/webm', 'audio').catch(() => null); | ||
| await RecStore.deleteSession(session.id); | ||
| await maybeGemini(audioBlob && audioBlob.size ? audioBlob : blob, name, saved.folderId); |
| downloadLocally(blob, name); | ||
| setStatus('Відновлено ✓ — файл завантажується'); | ||
| await RecStore.deleteSession(session.id); | ||
| } |
| function openDb() { | ||
| if (dbPromise) return dbPromise; | ||
| dbPromise = new Promise((resolve, reject) => { | ||
| const req = indexedDB.open(DB_NAME, DB_VERSION); | ||
| req.onupgradeneeded = () => { | ||
| const db = req.result; | ||
| if (!db.objectStoreNames.contains('recordings')) { | ||
| db.createObjectStore('recordings', { keyPath: 'id' }); | ||
| } | ||
| let chunks; | ||
| if (!db.objectStoreNames.contains('chunks')) { | ||
| // autoIncrement → монотонний порядок вставки; індекс 'rid' групує шматки сесії. | ||
| chunks = db.createObjectStore('chunks', { keyPath: 'seq', autoIncrement: true }); | ||
| chunks.createIndex('rid', 'rid', { unique: false }); | ||
| } else { | ||
| chunks = req.transaction.objectStore('chunks'); | ||
| } | ||
| if (!chunks.indexNames.contains('rid_kind')) { | ||
| chunks.createIndex('rid_kind', ['rid', 'kind'], { unique: false }); | ||
| } | ||
| }; | ||
| req.onsuccess = () => resolve(req.result); | ||
| req.onerror = () => reject(req.error || new Error('indexedDB open failed')); | ||
| }); | ||
| return dbPromise; | ||
| } |
| if (job.ticks >= GEMINI_MAX_TICKS) { | ||
| await finishGeminiJob('Конспект не вдалося зробити: тайм-аут обробки відео'); | ||
| } else { |
| if (file.state === 'FAILED') { | ||
| await finishGeminiJob('Конспект не вдалося зробити: Gemini не обробив відео'); | ||
| return; | ||
| } |
| Технічна примітка: надсилається саме відео (`video/webm`) з `mediaResolution: LOW` — бо | ||
| MediaRecorder дає `webm/opus`, який Gemini не приймає як аудіо, а конвертація в service | ||
| worker неможлива (немає Web Audio API). Низька роздільність кадрів тримає запит у межах | ||
| лімітів навіть для довгих зустрічей. | ||
| `MediaRecorder` дає `webm/opus`, який Gemini не приймає як чисте аудіо. Відео заливається в | ||
| Gemini Files API прямо з content script (де воно вже в пам'яті), а очікування обробки й | ||
| генерацію конспекту веде service worker фоново через `chrome.alarms`, тож вкладку Meet можна | ||
| закрити одразу. Низька роздільність кадрів тримає запит у межах лімітів навіть для довгих | ||
| зустрічей. |
| async function initRecovery() { | ||
| try { | ||
| await RecStore.pruneOld(); | ||
| const orphans = await RecStore.listOrphans(); | ||
| if (orphans.length && !isRecording) showRecoveryBanner(orphans[0]); | ||
| } catch (e) { | ||
| console.warn('[MeetRec] recovery init:', e); | ||
| } | ||
| } |
There was a problem hiding this comment.
Конкурентний конфлікт відновлення між вкладками
listOrphans() повертає всі сесії з IndexedDB без фільтрації за станом. Якщо користувач відкриє дві вкладки Google Meet (одна веде запис, друга — відкрита щойно), initRecovery в другій вкладці побачить активну сесію першої вкладки як "осиротілу" і показуватиме банер відновлення. При натисканні «✕» → dismissRecovery → RecStore.deleteSession(session.id) — дані активного запису в IndexedDB видаляються, і перша вкладка вже не зможе відновити запис після аварійного завершення.
Щоб усунути проблему, потрібно або зберігати tabId у метаданих сесії і порівнювати з chrome.tabs через background, або додати поле heartbeat (оновлюється кожні N секунд під час запису) і при initRecovery перевіряти, чи не оновлювався heartbeat нещодавно.
| // Аудіо-доріжку з журналу шлемо в Gemini; якщо її нема (старі сесії) — фолбек на відео. | ||
| const audioBlob = await RecStore.readBlob(session.id, 'audio/webm', 'audio').catch(() => null); | ||
| await RecStore.deleteSession(session.id); | ||
| await maybeGemini(audioBlob && audioBlob.size ? audioBlob : blob, name, saved.folderId); |
There was a problem hiding this comment.
Неправильний MIME-тип при відновленні: відео-blob передається в Gemini як
audio/webm
У рядку 344 функція recover передає резервний відео-blob у maybeGemini, коли аудіо-доріжка відсутня (старі сесії без окремого треку). Але maybeGemini безумовно викликає Gemini.geminiUploadFile(audioBlob, geminiApiKey, 'audio/webm') — тобто відео-контент маркується заголовком X-Goog-Upload-Header-Content-Type: audio/webm. Gemini Files API відхилить або неправильно оброблятиме відео-файл з таким MIME-типом, і конспект або не буде згенерований, або генерація поверне помилку.
| const safe = name.replace(/'/g, "\\'"); | ||
| let q = `mimeType='application/vnd.google-apps.folder' and name='${safe}' and trashed=false`; |
There was a problem hiding this comment.
Неповна екранізація символів у Drive API-запиті
Поточна санітизація name.replace(/'/g, "\\'") обробляє лише одинарні лапки, але не зворотні слеші. Якщо в назві папки з'явиться послідовність \', подвоєний слеш може залишити незакриту цитату в рядку Drive API-запиту. Безпечніший варіант — екранувати обидва символи: спочатку \\, потім '.
| } catch (driveErr) { | ||
| console.warn('[MeetRec] Drive недоступний, зберігаю локально:', driveErr); | ||
| downloadLocally(blob, name); | ||
| return { where: 'local', folderId: null }; | ||
| } |
There was a problem hiding this comment.
Помилки
downloadLocally мовчки ковтаються
downloadLocally(blob, name) викликається синхронно без await і без обгортки try/catch. Якщо програмний клік <a download> заблокований, користувач отримає статус «Готово ✓ — збережено локально» без реального збереження файлу і без жодного сигналу про помилку.



Summary by cubic
Moves large uploads (Drive and Gemini) into the Meet page, adds crash-safe recording with recovery, and runs Gemini summarization in the background so you can close the tab. Improves reliability and reduces data shuffling for faster saves.
New Features
chrome.alarms; you can close the tab; summary saved to the same folder as a Google Doc (fallback.txt).Refactors
content.js; the service worker provides OAuth tokens (with refresh), badge/stop, and Gemini polling.gdrive.js,gemini.js, andrecstore.js.manifest.json: addedalarms; loadrecstore.js,gdrive.js,gemini.jsbeforecontent.js.rid_kindindex for cheap duration estimates.Written for commit 46e98f9. Summary will update on new commits.
Summary by CodeRabbit
Нові функції
Виправлення помилок
Документація
Greptile Summary
PR рефакторить архітектуру розширення: великі мережеві операції (Drive upload, Gemini upload) переміщено з service worker у content script, щоб уникнути передачі відео-blob через
sendMessage. Фонова обробка конспектів тепер ведеться черезchrome.alarms, що дозволяє закрити вкладку Meet після завершення запису.recstore.js(IndexedDB) пише шматки MediaRecorder на диск під час запису; при аварійному завершенні вкладки записані дані відновлюються банером при наступному відкритті Meet.gdrive.js) і Gemini (gemini.js) винесено в окремі чисті модулі, спільні для content script і service worker без дублювання.Confidence Score: 3/5
Нова функція відновлення містить два дефекти в content.js — перед мерджем їх слід усунути.
Механізм відновлення у listOrphans не розрізняє активні та осиротілі сесії — сценарій з двома вкладками Meet дає банер відновлення поверх живого запису з можливістю видалити IndexedDB-дані активної сесії. Другий дефект: при відновленні старих сесій без аудіо-доріжки відео-blob надсилається в Gemini з MIME-типом audio/webm, що призводить до помилки генерації конспекту.
content.js — функції initRecovery, recover та maybeGemini; recstore.js — listOrphans.
Important Files Changed
Reviews (1): Last reviewed commit: "Enhance audio recording capabilities in ..." | Re-trigger Greptile
Context used: