您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在YouTube视频中添加按钮,点击后跳转到Gemini并自动输入提示词总结视频 (Optimized for speed)
当前为
// ==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 // ==/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 <textarea> } else { // Method 3: Fallback using execCommand (less reliable now) document.execCommand('insertText', false, prompt); } // Trigger events to make sure the framework detects the change textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); inputSuccess = true; debugLog("Prompt inserted into textarea."); } catch (inputError) { debugLog(`Error inserting text: ${inputError}. Trying clipboard fallback.`); showGeminiNotification("无法自动填入提示词。请手动粘贴。\n提示词已复制到剪贴板。", "error"); copyToClipboard(prompt); // Clean up storage as we failed GM_deleteValue('geminiPrompt'); GM_deleteValue('timestamp'); GM_deleteValue('videoTitle'); return; // Stop further execution if input fails } if (inputSuccess) { // Wait a very short moment for UI to potentially update after input await new Promise(resolve => setTimeout(resolve, 100)); // 100ms delay debugLog("Waiting for send button..."); // Use the original selectors const sendButtonSelectors = [ 'button:has(mat-icon[data-mat-icon-name="send"])', // Icon inside button 'mat-icon[data-mat-icon-name="send"]', // The icon itself (might need parent click) 'button:has(span.mat-mdc-button-touch-target)', // Material components structure 'button.mat-mdc-icon-button', // General Material icon button 'button[id*="submit"]', // Buttons with 'submit' in ID // Fallbacks based on aria-label (language-dependent) 'button[aria-label="Run"]', 'button[aria-label="Send"]', 'button[aria-label="Submit"]', 'button[aria-label="发送"]' // Chinese label ]; try { // Wait for the send button to appear and be clickable let sendButtonElement = await waitForElements(sendButtonSelectors, GEMINI_ELEMENT_TIMEOUT_MS); debugLog("Send button found."); // If the found element is the icon, get the parent button if (sendButtonElement.tagName.toLowerCase() === 'mat-icon') { const parentButton = sendButtonElement.closest('button'); if (parentButton && !parentButton.disabled) { sendButtonElement = parentButton; } else { throw new Error("Send icon found, but parent button is missing or disabled."); } } // Check if the button is enabled if (sendButtonElement.disabled) { debugLog("Send button is disabled. Waiting a bit longer..."); // Wait a bit more, maybe it enables after input validation await new Promise(resolve => setTimeout(resolve, 500)); if (sendButtonElement.disabled) { throw new Error("Send button remained disabled."); } } // Click the button sendButtonElement.click(); debugLog("Send button clicked successfully."); // Success notification const successMessage = ` 已自动发送视频摘要请求! 正在分析视频: "${videoTitle}" 请稍候,Gemini正在处理您的请求... `; showGeminiNotification(successMessage.trim(), "info"); // Clean up storage after successful operation GM_deleteValue('geminiPrompt'); GM_deleteValue('timestamp'); GM_deleteValue('videoTitle'); } catch (buttonError) { debugLog(`Send button error: ${buttonError.message}`); showGeminiNotification("找不到或无法点击发送按钮。\n提示词已填入,请手动点击发送。", "warning"); // Keep prompt in storage for manual use if button click fails } } } catch (textareaError) { debugLog(`Textarea error: ${textareaError.message}`); showGeminiNotification("无法找到Gemini输入框。\n请手动粘贴提示词。\n提示词已复制到剪贴板。", "error"); copyToClipboard(prompt); // Ensure prompt is available // Clean up storage as we failed early GM_deleteValue('geminiPrompt'); GM_deleteValue('timestamp'); GM_deleteValue('videoTitle'); } } // --- Main Execution Logic --- const isYouTube = window.location.hostname.includes('youtube.com') || window.location.hostname.includes('www.youtube.com'); // Added www.youtube.com just in case const isGemini = window.location.hostname.includes('gemini.google.com'); if (isYouTube) { debugLog("YouTube page detected. Initializing button adder."); // Initial attempt to add the button addSummarizeButton(); // Observe URL changes for SPA navigation let lastUrl = location.href; const observer = new MutationObserver(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; debugLog(`URL changed to: ${currentUrl}. Re-checking for button placement.`); // Wait a brief moment for the new page content to potentially load setTimeout(addSummarizeButton, 500); // Reduced delay } }); // Observe the body for broader changes, but consider performance if issues arise observer.observe(document.body, { childList: true, subtree: true }); debugLog("MutationObserver set up for URL changes."); } else if (isGemini) { // Use window.onload or a short delay to ensure GM_getValue is ready after page load // document-idle should be sufficient, but adding a small safety delay. setTimeout(handleGemini, 500); // Start Gemini logic slightly after idle } })();