"use strict"; (async () => { // This file cannot import config.js or logger.js directly because it runs in a different context (source:https://stackoverflow.com/a/53033388/4235602) // So we need to load them dynamically. But our config.js and logger.js are in the same directory and can import each other try { //#region load logger const loggerSrc = chrome.runtime.getURL("logger.js"); const loggerModule = await import(loggerSrc); const logger = loggerModule.default; logger.debug("Logger initialized successfully"); logger.debug("Content script starting..."); //#endregion //#region load config const configSrc = chrome.runtime.getURL("config.js"); const configModule = await import(configSrc); const config = configModule.default; logger.debug("Config loaded:", config); //#endregion class CaptionCapture { constructor() { this.isCapturing = false; this.intervalId = null; this.lastSpans = []; this.captionsLog = []; this.mergedCaption = []; this.subtitleText = null; this.checkSubtitleInterval = config.defaultCaptureInterval; } // Function to detect and get the caption spans as an array detectCaptionAsArray() { try { let spans = document.querySelectorAll('div.ygicle.VbkSUe'); if (!spans || spans.length === 0) { logger.debug("No caption spans found"); return []; } return Array.from(spans).map(span => span.innerText.trim()) || []; } catch (error) { logger.error("Error detecting captions:", error); return []; } } // Function to get absolute time in HH:MM:SS,mmm format (Real-Time) getCurrentTimestamp() { let now = new Date(); let hours = String(now.getHours()).padStart(2, '0'); let minutes = String(now.getMinutes()).padStart(2, '0'); let seconds = String(now.getSeconds()).padStart(2, '0'); let millis = String(now.getMilliseconds()).padStart(3, '0'); return `${hours}:${minutes}:${seconds},${millis}`; // Absolute time format } // Function to save new words with timestamp saveCaptionByWords(timestamp, newText) { this.captionsLog.push({ time: timestamp, text: newText }); logger.debug(`${timestamp} --> ${newText}`); // Print in subtitle format } formatTimestamp(ms) { const h = String(Math.floor(ms / 3600000)).padStart(2, "0"); ms %= 3600000; const m = String(Math.floor(ms / 60000)).padStart(2, "0"); ms %= 60000; const s = String(Math.floor(ms / 1000)).padStart(2, "0"); const msStr = String(ms % 1000).padStart(3, "0"); return `${h}:${m}:${s},${msStr}`; } mergeCaptions(captions, gap = 1000) { if (!captions || captions.length === 0) return []; const parseTime = (timestamp) => { const [h, m, sMs] = timestamp.split(":"); const [s, ms] = sMs.split(","); return ( parseInt(h) * 3600000 + parseInt(m) * 60000 + parseInt(s) * 1000 + parseInt(ms) ); }; const merged = []; let current = { start: parseTime(captions[0].time), end: parseTime(captions[0].time), texts: [captions[0].text] }; for (let i = 1; i < captions.length; i++) { const entry = captions[i]; const time = parseTime(entry.time); const timeDiff = time - current.end; if (timeDiff > gap) { merged.push({ start: this.formatTimestamp(current.start), end: this.formatTimestamp(current.end), text: current.texts.join(" ") }); current = { start: time, end: time, texts: [entry.text] }; } else { current.end = time; current.texts.push(entry.text); } } // Push the last group merged.push({ start: this.formatTimestamp(current.start), end: this.formatTimestamp(current.end), text: current.texts.join(" ") }); this.mergedCaption = merged; return merged; } printMergedSubtitle() { const merged = this.mergeCaptions(this.getCapturedCaptions(), 1000); if (merged.length === 0) { logger.debug("No subtitles to print."); return; } const parseTime = (timestamp) => { const [h, m, sMs] = timestamp.split(":"); const [s, ms] = sMs.split(","); return ( parseInt(h) * 3600000 + parseInt(m) * 60000 + parseInt(s) * 1000 + parseInt(ms) ); }; const baseTime = parseTime(merged[0].start); // time of first word const toRelative = (absTime) => { const ms = parseTime(absTime) - baseTime; return this.formatTimestamp(ms); }; this.subtitleText = merged.map((item, index) => `${index + 1}\n${toRelative(item.start)} --> ${toRelative(item.end)}\n${item.text}\n` ).join("\n"); logger.debug(this.subtitleText); return this.subtitleText; } generateSrtFilename() { const now = new Date(); const pad = (n) => String(n).padStart(2, '0'); return `meet_caption_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}.srt`; } downloadSrtFile() { if (!this.subtitleText) { logger.warn("No subtitle content found. Please call printMergedSubtitle() first."); return; } const filename = this.generateSrtFilename(); const blob = new Blob([this.subtitleText], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } // Function to start capturing captions startCapturing() { if (this.isCapturing) { logger.debug("Caption capturing is already running."); return; } this.isCapturing = true; logger.debug("Caption capturing started."); this.intervalId = setInterval(() => { let currentSpans = this.detectCaptionAsArray(); let lastOldSpan = this.lastSpans[this.lastSpans.length - 1] || ""; let lastNewSpan = currentSpans[currentSpans.length - 1] || ""; let isLastCaptionChanged = lastNewSpan !== lastOldSpan; if (isLastCaptionChanged) { this.lastSpans = currentSpans; // Update stored spans let timestamp = this.getCurrentTimestamp(); // Get only the new part of the last span let newText = lastNewSpan.replace(lastOldSpan, "").trim(); if (newText) { logger.debug(newText) const wordCount = newText.trim().split(/\s+/).length; const BurstTextCount = 80; if (wordCount <= (this.checkSubtitleInterval / 100) * BurstTextCount) { this.saveCaptionByWords(timestamp, newText); } else { logger.debug(`Skipped burst text with ${wordCount} words at ${timestamp}`); } } } }, this.checkSubtitleInterval); // Runs every 100ms for better precision } // Function to stop capturing captions stopCapturing() { if (!this.isCapturing) { logger.debug("Caption capturing is not running."); return; } this.isCapturing = false; clearInterval(this.intervalId); logger.debug("Caption capturing stopped."); } // Function to get the saved captions log getCapturedCaptions() { return this.captionsLog; } } // Initialize CaptionCapture and message listener const captionCapture = new CaptionCapture(); logger.debug("CaptionCapture initialized"); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { logger.debug("start of listener in content.js"); if (message.action === "startCapture") { logger.debug("startCapture content.js"); captionCapture.startCapturing(); sendResponse({ status: "started" }); } else if (message.action === "stopCapture") { logger.debug("stopCapture content.js"); captionCapture.stopCapturing(); sendResponse({ status: "stopped" }); } else if (message.action === "downloadSrt") { alert("Downloading SRT file..."); logger.debug("downloadSrt content.js"); captionCapture.printMergedSubtitle(); captionCapture.downloadSrtFile(); sendResponse({ status: "downloadStarted" }); } }); logger.debug("Message listener set up"); } catch (error) { logger.error("Error loading modules:", error); } })();