// ==UserScript== // @name YouTube to Gemini Auto Summarizer // @namespace http://tampermonkey.net/ // @version 0.5 // @description 在YouTube视频中添加按钮,点击后跳转到Gemini并自动输入提示词总结视频 (Optimized for speed) // @author hengyu (Optimized by Assistant) // @match *://www.youtube.com/watch?* // @match *://gemini.google.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @run-at document-idle // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const CHECK_INTERVAL_MS = 100; // How often to check for elements (milliseconds) const YOUTUBE_ELEMENT_TIMEOUT_MS = 10000; // Max time to wait for YouTube elements (milliseconds) const GEMINI_ELEMENT_TIMEOUT_MS = 15000; // Max time to wait for Gemini elements (milliseconds) const GEMINI_PROMPT_EXPIRY_MS = 300000; // 5 minutes validity for the prompt transfer // --- Debug Logging --- function debugLog(message) { console.log(`[YouTube to Gemini] ${message}`); } // --- Helper Functions --- function waitForElement(selector, timeoutMs, parent = document) { return new Promise((resolve, reject) => { let element = parent.querySelector(selector); if (element && element.offsetWidth > 0 && element.offsetHeight > 0) { return resolve(element); } const intervalId = setInterval(() => { element = parent.querySelector(selector); if (element && element.offsetWidth > 0 && element.offsetHeight > 0) { clearInterval(intervalId); clearTimeout(timeoutId); resolve(element); } }, CHECK_INTERVAL_MS); const timeoutId = setTimeout(() => { clearInterval(intervalId); debugLog(`Element not found or not visible after ${timeoutMs}ms: ${selector}`); reject(new Error(`Element not found or not visible: ${selector}`)); }, timeoutMs); }); } function waitForElements(selectors, timeoutMs, parent = document) { return new Promise((resolve, reject) => { let foundElement = null; const startTime = Date.now(); function checkElements() { for (const selector of selectors) { const elements = parent.querySelectorAll(selector); for (const el of elements) { // Check for visibility (basic check) if (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0) { // Additional check for send button state if applicable if (selectors.some(s => s.includes('button')) && el.disabled) { continue; // Skip disabled buttons if looking for a send button } foundElement = el; break; } } if (foundElement) break; } if (foundElement) { clearInterval(intervalId); clearTimeout(timeoutId); resolve(foundElement); } else if (Date.now() - startTime > timeoutMs) { clearInterval(intervalId); debugLog(`Elements not found or not visible after ${timeoutMs}ms: ${selectors.join(', ')}`); reject(new Error(`Elements not found or not visible: ${selectors.join(', ')}`)); } } const intervalId = setInterval(checkElements, CHECK_INTERVAL_MS); const timeoutId = setTimeout(() => { clearInterval(intervalId); if (!foundElement) { debugLog(`Elements not found or not visible after ${timeoutMs}ms: ${selectors.join(', ')}`); reject(new Error(`Elements not found or not visible: ${selectors.join(', ')}`)); } }, timeoutMs); // Initial check checkElements(); }); } function copyToClipboard(text) { const textarea = document.createElement('textarea'); textarea.value = text; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); } catch (err) { debugLog('Failed to copy to clipboard using execCommand.'); // Fallback or notification could be added here } document.body.removeChild(textarea); } function showNotification(elementId, message, styles, duration = 15000) { // Remove existing notification first let existingNotification = document.getElementById(elementId); if (existingNotification) { document.body.removeChild(existingNotification); } const notification = document.createElement('div'); notification.id = elementId; notification.innerText = message; Object.assign(notification.style, styles); // Apply base styles document.body.appendChild(notification); // Add close button const closeButton = document.createElement('button'); closeButton.innerText = '✕'; // Basic styling, adjust as needed closeButton.style.position = 'absolute'; closeButton.style.top = '5px'; closeButton.style.right = '10px'; closeButton.style.background = 'transparent'; closeButton.style.border = 'none'; closeButton.style.color = 'inherit'; // Inherit color from notification closeButton.style.fontSize = '16px'; closeButton.style.cursor = 'pointer'; closeButton.onclick = function() { if (document.body.contains(notification)) { document.body.removeChild(notification); } }; notification.appendChild(closeButton); // Auto-remove after duration const timeoutId = setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, duration); // Store timeout ID if needed for cancellation notification.dataset.timeoutId = timeoutId; } // --- YouTube Specific --- const YOUTUBE_NOTIFICATION_ID = 'gemini-yt-notification'; const YOUTUBE_NOTIFICATION_STYLE = { position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', backgroundColor: 'rgba(0,0,0,0.8)', color: 'white', padding: '20px', borderRadius: '8px', zIndex: '9999', maxWidth: '80%', textAlign: 'left', whiteSpace: 'pre-line' }; function addSummarizeButton() { // Check if it's a watch page (adjust if URL structure is different) // Using the provided URL pattern from the original script for consistency if (!window.location.href.includes('youtube.com/watch')) { debugLog("Not a watch page (based on URL check), button not added."); return; } if (document.getElementById('gemini-summarize-btn')) { debugLog("Summarize button already exists."); return; } // Use the original selector, wait for it waitForElement('#masthead #end', YOUTUBE_ELEMENT_TIMEOUT_MS) .then(container => { if (document.getElementById('gemini-summarize-btn')) return; // Double check const button = document.createElement('button'); button.id = 'gemini-summarize-btn'; button.innerText = '📝 Gemini摘要'; // Apply original styles Object.assign(button.style, { backgroundColor: '#2F80ED', color: 'white', border: 'none', borderRadius: '4px', padding: '8px 16px', margin: '0 16px', cursor: 'pointer', fontWeight: 'bold', height: '36px', // Match YouTube's button height display: 'flex', alignItems: 'center' }); button.addEventListener('click', function() { const youtubeUrl = window.location.href; // Attempt to get a cleaner title const videoTitle = document.querySelector('h1.ytd-watch-metadata')?.textContent?.trim() || document.title.replace(' - YouTube', ''); const prompt = `请分析这个YouTube视频: ${youtubeUrl}\n\n提供一个全面的摘要,包括主要观点、关键见解和视频中讨论的重要细节,以结构化的方式分解内容,并包括任何重要的结论或要点。`; GM_setValue('geminiPrompt', prompt); GM_setValue('videoTitle', videoTitle); GM_setValue('timestamp', Date.now()); // Open Gemini in a new tab window.open('https://gemini.google.com/', '_blank'); const notificationMessage = ` 已跳转到Gemini! 系统将尝试自动输入并发送提示。 如果自动操作失败,提示词已复制到剪贴板,您可以手动粘贴。 视频: "${videoTitle}" `; showNotification(YOUTUBE_NOTIFICATION_ID, notificationMessage.trim(), YOUTUBE_NOTIFICATION_STYLE); copyToClipboard(prompt); // Backup copy }); // Add the button to the container container.insertBefore(button, container.firstChild); debugLog("Summarize button added successfully."); }) .catch(error => { debugLog(`Could not add YouTube button: ${error.message}`); }); } // --- Gemini Specific --- const GEMINI_NOTIFICATION_ID = 'gemini-auto-notification'; const GEMINI_NOTIFICATION_STYLES = { info: { backgroundColor: '#e8f4fd', color: '#0866c2', border: '1px solid #b8daff' }, warning: { backgroundColor: '#fff3e0', color: '#b35d00', border: '1px solid #ffe0b2' }, error: { backgroundColor: '#fdecea', color: '#c62828', border: '1px solid #ffcdd2' } }; const BASE_GEMINI_NOTIFICATION_STYLE = { position: 'fixed', bottom: '20px', right: '20px', padding: '15px 20px', borderRadius: '8px', zIndex: '9999', maxWidth: '350px', textAlign: 'left', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', whiteSpace: 'pre-line' }; function showGeminiNotification(message, type = "info") { const style = { ...BASE_GEMINI_NOTIFICATION_STYLE, ...(GEMINI_NOTIFICATION_STYLES[type] || GEMINI_NOTIFICATION_STYLES.info) }; showNotification(GEMINI_NOTIFICATION_ID, message, style, 10000); } async function handleGemini() { debugLog("Gemini page detected. Checking for prompt..."); const prompt = GM_getValue('geminiPrompt', ''); const timestamp = GM_getValue('timestamp', 0); const videoTitle = GM_getValue('videoTitle', 'N/A'); // Check if prompt exists and is recent if (!prompt || Date.now() - timestamp > GEMINI_PROMPT_EXPIRY_MS) { debugLog("No valid prompt found in storage or it expired."); GM_deleteValue('geminiPrompt'); // Clean up expired/invalid prompt GM_deleteValue('timestamp'); GM_deleteValue('videoTitle'); return; } debugLog("Valid prompt found. Waiting for Gemini input area..."); // Use the original selectors from the script const textareaSelectors = [ 'div[class*="text-input-field"][class*="with-toolbox-drawer"]', // Specific from screenshot 'div[class*="input-area"]', // General area 'div[contenteditable="true"]', // Content editable divs often used 'div[class*="textarea-wrapper"]', 'textarea', // Standard textarea 'div[role="textbox"]' // Accessibility role ]; try { // Wait for the textarea to appear and be interactable const textarea = await waitForElements(textareaSelectors, GEMINI_ELEMENT_TIMEOUT_MS); debugLog("Textarea found. Attempting to input prompt."); // Input the text - trying different methods for compatibility let inputSuccess = false; try { textarea.focus(); // Focus first if (textarea.isContentEditable) { textarea.innerText = prompt; // Method 1: for contentEditable divs } else if (textarea.tagName.toLowerCase() === 'textarea') { textarea.value = prompt; // Method 2: for