// ==UserScript== // @name YouTube to Gemini 自动总结与字幕 // @namespace http://tampermonkey.net/ // @version 0.9.8 // @description 在YouTube视频中添加按钮,点击后跳转到Gemini并自动输入提示词总结视频或生成字幕。长视频字幕首段自动处理,后续手动。 // @author hengyu (Optimized by Assistant) // @match *://www.youtube.com/* // @match *://gemini.google.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addStyle // @run-at document-start // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // --- 配置 --- const CHECK_INTERVAL_MS = 200; // Fallback polling interval const YOUTUBE_ELEMENT_TIMEOUT_MS = 10000; // 等待YouTube元素的最大时间(毫秒) const GEMINI_ELEMENT_TIMEOUT_MS = 15000; // 等待Gemini元素的最大时间(毫秒) const GEMINI_PROMPT_EXPIRY_MS = 300000; // 提示词传输有效期5分钟 const SUBTITLE_SEGMENT_DURATION_SECONDS = 1200; // 字幕分段时长,20分钟 = 1200秒 // Key selectors for ensuring page/video context is ready const YOUTUBE_PLAYER_METADATA_SELECTOR = 'ytd-watch-metadata, #above-the-fold .title'; // --- GM存储键 --- const PROMPT_KEY = 'geminiPrompt'; const TITLE_KEY = 'videoTitle'; // This will store the title, potentially with segment info const ORIGINAL_TITLE_KEY = 'geminiOriginalVideoTitle'; // Separate key for the pure original title const TIMESTAMP_KEY = 'timestamp'; const ACTION_TYPE_KEY = 'geminiActionType'; // 'summary' or 'subtitle' const VIDEO_TOTAL_DURATION_KEY = 'geminiVideoTotalDuration'; const FIRST_SEGMENT_END_TIME_KEY = 'geminiFirstSegmentEndTime'; // --- 调试日志 --- const DEBUG = true; // Enable for detailed logging function debugLog(message) { if (DEBUG) { console.log(`[YT->Gemini Optimized V0.9.9] ${message}`); } } // --- 辅助函数 --- function formatTimeHHMMSS(totalSeconds) { if (isNaN(totalSeconds) || totalSeconds < 0) { return '00:00:00'; // Default or error value } const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = Math.floor(totalSeconds % 60); const pad = (num) => String(num).padStart(2, '0'); return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; } function parseISO8601DurationToSeconds(durationString) { if (!durationString || typeof durationString !== 'string' || !durationString.startsWith('PT')) return 0; let totalSeconds = 0; const timePart = durationString.substring(2); const hourMatch = timePart.match(/(\d+)H/); if (hourMatch) totalSeconds += parseInt(hourMatch[1]) * 3600; const minuteMatch = timePart.match(/(\d+)M/); if (minuteMatch) totalSeconds += parseInt(minuteMatch[1]) * 60; const secondMatch = timePart.match(/(\d+)S/); if (secondMatch) totalSeconds += parseInt(secondMatch[1]); return totalSeconds; } /** * Waits for one or more elements matching the selectors to appear and be visible in the DOM. * Prioritizes MutationObserver for efficiency, falls back to polling if needed. * @param {string|string[]} selectors - A CSS selector string or an array of selectors. * @param {number} timeoutMs - Maximum time to wait in milliseconds. * @param {Element} [parent=document] - The parent element to search within. * @returns {Promise} A promise that resolves with the found element. */ function waitForElement(selectors, timeoutMs, parent = document) { const selectorArray = Array.isArray(selectors) ? selectors : [selectors]; const combinedSelector = selectorArray.join(', '); // Efficiently query all at once return new Promise((resolve, reject) => { const initialElement = findVisibleElement(combinedSelector, parent); if (initialElement) { debugLog(`Element found immediately: ${combinedSelector}`); return resolve(initialElement); } let observer = null; let timeoutId = null; const cleanup = () => { if (observer) { observer.disconnect(); observer = null; debugLog(`MutationObserver disconnected for: ${combinedSelector}`); } if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } }; const onTimeout = () => { cleanup(); debugLog(`Element not found or not visible after ${timeoutMs}ms: ${combinedSelector}`); reject(new Error(`Element not found or not visible: ${combinedSelector}`)); }; const checkNode = (node) => { if (node && node.nodeType === Node.ELEMENT_NODE) { if (node.matches(combinedSelector) && isElementVisible(node)) { debugLog(`Element found via MutationObserver (direct match): ${combinedSelector}`); cleanup(); resolve(node); return true; } const foundDescendant = findVisibleElement(combinedSelector, node); if (foundDescendant) { debugLog(`Element found via MutationObserver (descendant): ${combinedSelector}`); cleanup(); resolve(foundDescendant); return true; } } return false; }; timeoutId = setTimeout(onTimeout, timeoutMs); observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (checkNode(node)) return; } } else if (mutation.type === 'attributes') { if (checkNode(mutation.target)) return; } } const element = findVisibleElement(combinedSelector, parent); if (element) { debugLog(`Element found via MutationObserver (fallback check): ${combinedSelector}`); cleanup(); resolve(element); } }); observer.observe(parent === document ? document.documentElement : parent, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'disabled'] }); debugLog(`MutationObserver started for: ${combinedSelector}`); }); } function findVisibleElement(selector, parent) { try { const elements = parent.querySelectorAll(selector); for (const el of elements) { if (isElementVisible(el)) { if (selector.includes('button') && el.disabled) { continue; } return el; } } } catch (e) { debugLog(`Error finding element with selector "${selector}": ${e}`); } return null; } function isElementVisible(el) { if (!el) return false; return (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0); } function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { debugLog("Text copied to clipboard via modern API."); }).catch(err => { debugLog(`Clipboard API failed: ${err}, using legacy method.`); legacyClipboardCopy(text); }); } function legacyClipboardCopy(text) { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.top = '-9999px'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); textarea.select(); try { const successful = document.execCommand('copy'); debugLog(`Legacy copy attempt: ${successful ? 'Success' : 'Fail'}`); } catch (err) { debugLog('Failed to copy to clipboard using legacy execCommand: ' + err); } document.body.removeChild(textarea); } function showNotification(elementId, message, styles, duration = 15000) { let existingNotification = document.getElementById(elementId); if (existingNotification) { const existingTimeoutId = existingNotification.dataset.timeoutId; if (existingTimeoutId) { clearTimeout(parseInt(existingTimeoutId)); } existingNotification.remove(); } const notification = document.createElement('div'); notification.id = elementId; notification.textContent = message; Object.assign(notification.style, styles); document.body.appendChild(notification); const closeButton = document.createElement('button'); closeButton.textContent = '✕'; Object.assign(closeButton.style, { position: 'absolute', top: '5px', right: '10px', background: 'transparent', border: 'none', color: 'inherit', fontSize: '16px', cursor: 'pointer', padding: '0', lineHeight: '1' }); closeButton.onclick = () => notification.remove(); notification.appendChild(closeButton); const timeoutId = setTimeout(() => notification.remove(), duration); notification.dataset.timeoutId = timeoutId.toString(); } // --- YouTube Related --- 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.85)', color: 'white', padding: '15px 35px 15px 20px', borderRadius: '8px', zIndex: '9999', maxWidth: 'calc(100% - 40px)', textAlign: 'left', boxSizing: 'border-box', whiteSpace: 'pre-wrap', boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }; const SUMMARY_BUTTON_ID = 'gemini-summarize-btn'; const SUBTITLE_BUTTON_ID = 'gemini-subtitle-btn'; const THUMBNAIL_BUTTON_CLASS = 'gemini-thumbnail-btn'; GM_addStyle(` .${THUMBNAIL_BUTTON_CLASS} { position: absolute; top: 5px; right: 5px; background-color: rgba(0, 0, 0, 0.7); color: white; border: none; border-radius: 4px; padding: 4px 8px; font-size: 12px; cursor: pointer; z-index: 100; display: flex; align-items: center; opacity: 0; transition: opacity 0.2s ease; } #dismissible:hover .${THUMBNAIL_BUTTON_CLASS}, ytd-grid-video-renderer:hover .${THUMBNAIL_BUTTON_CLASS}, ytd-video-renderer:hover .${THUMBNAIL_BUTTON_CLASS}, ytd-rich-item-renderer:hover .${THUMBNAIL_BUTTON_CLASS}, ytd-compact-video-renderer:hover .${THUMBNAIL_BUTTON_CLASS} { opacity: 1; } .${THUMBNAIL_BUTTON_CLASS}:hover { background-color: rgba(0, 0, 0, 0.9); } `); function isVideoPage() { return window.location.pathname === '/watch' && new URLSearchParams(window.location.search).has('v'); } function getVideoInfoFromElement(element) { try { let videoId = ''; const linkElement = element.querySelector('a[href*="/watch?v="]'); if (linkElement) { const href = linkElement.getAttribute('href'); const match = href.match(/\/watch\?v=([^&]+)/); if (match && match[1]) { videoId = match[1]; } } let videoTitle = ''; const titleElement = element.querySelector('#video-title, .title, [title]'); if (titleElement) { videoTitle = titleElement.textContent?.trim() || titleElement.getAttribute('title')?.trim() || ''; } if (!videoId || !videoTitle) { return null; } return { id: videoId, title: videoTitle, url: `https://www.youtube.com/watch?v=${videoId}` }; } catch (error) { console.error('获取视频信息时出错:', error); return null; } } function handleThumbnailButtonClick(event, videoInfo) { event.preventDefault(); event.stopPropagation(); try { if (!videoInfo || !videoInfo.url || !videoInfo.title) { throw new Error('视频信息不完整'); } const prompt = `请分析这个YouTube视频: ${videoInfo.url}\n\n提供一个全面的摘要,包括主要观点、关键见解和视频中讨论的重要细节,以结构化的方式分解内容,并包括任何重要的结论或要点。`; debugLog(`从缩略图生成提示词: ${videoInfo.title}`); GM_setValue(PROMPT_KEY, prompt); GM_setValue(TITLE_KEY, videoInfo.title); GM_setValue(ORIGINAL_TITLE_KEY, videoInfo.title); GM_setValue(TIMESTAMP_KEY, Date.now()); GM_setValue(ACTION_TYPE_KEY, 'summary'); // Set action type window.open('https://gemini.google.com/', '_blank'); debugLog("从缩略图打开Gemini标签页。"); const notificationMessage = ` 已跳转到 Gemini! 系统将尝试自动输入提示词并发送请求。 视频: "${videoInfo.title}" (如果自动操作失败,提示词已复制到剪贴板,请手动粘贴) `.trim(); showNotification(YOUTUBE_NOTIFICATION_ID, notificationMessage, YOUTUBE_NOTIFICATION_STYLE, 10000); copyToClipboard(prompt); } catch (error) { console.error("[YT->Gemini Optimized] 处理缩略图按钮点击时出错:", error); showNotification(YOUTUBE_NOTIFICATION_ID, `创建摘要时出错: ${error.message}`, { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025', color: 'white' }, 10000); } } function addThumbnailButtons() { const videoElementSelectors = [ 'ytd-rich-item-renderer', 'ytd-grid-video-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'ytd-playlist-video-renderer' ]; const videoElements = document.querySelectorAll(videoElementSelectors.join(',')); videoElements.forEach(element => { if (element.querySelector(`.${THUMBNAIL_BUTTON_CLASS}`)) { return; } const thumbnailContainer = element.querySelector('#thumbnail, .thumbnail, a[href*="/watch"]'); if (!thumbnailContainer) { return; } const videoInfo = getVideoInfoFromElement(element); if (!videoInfo) { return; } const button = document.createElement('button'); button.className = THUMBNAIL_BUTTON_CLASS; button.textContent = '📝 总结'; button.title = '使用Gemini总结此视频'; button.addEventListener('click', (e) => handleThumbnailButtonClick(e, videoInfo)); thumbnailContainer.style.position = 'relative'; thumbnailContainer.appendChild(button); }); } function setupThumbnailButtonObserver() { addThumbnailButtons(); const observer = new MutationObserver((mutations) => { let shouldAddButtons = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName && ( node.tagName.toLowerCase().includes('ytd-') || node.querySelector('ytd-rich-item-renderer, ytd-grid-video-renderer, ytd-video-renderer') )) { shouldAddButtons = true; break; } } } } if (shouldAddButtons) break; } if (shouldAddButtons) { clearTimeout(window.thumbnailButtonTimeout); window.thumbnailButtonTimeout = setTimeout(addThumbnailButtons, 200); } }); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('yt-navigate-finish', () => { debugLog("检测到页面导航,添加缩略图按钮"); setTimeout(addThumbnailButtons, 300); }); } async function addYouTubeActionButtons() { // This function's internal check for isVideoPage() is now redundant // because runYouTubeLogic handles this before calling. // However, keeping it doesn't harm and adds a layer of safety. if (!isVideoPage()) { debugLog("addYouTubeActionButtons called on non-video page, ensuring removal."); removeYouTubeActionButtonsIfExists(); return; } if (document.getElementById(SUMMARY_BUTTON_ID) || document.getElementById(SUBTITLE_BUTTON_ID)) { debugLog("Action buttons already exist."); return; } debugLog("Video page detected. Attempting to add action buttons..."); const containerSelectors = [ '#top-row.ytd-watch-metadata > #subscribe-button', '#meta-contents #subscribe-button', '#owner #subscribe-button', '#meta-contents #top-row', '#above-the-fold #title', 'ytd-watch-metadata #actions', '#masthead #end' ]; try { const anchorElement = await waitForElement(containerSelectors, YOUTUBE_ELEMENT_TIMEOUT_MS); debugLog(`Found anchor element using selector matching: ${anchorElement.tagName}[id="${anchorElement.id}"][class="${anchorElement.className}"]`); if (document.getElementById(SUMMARY_BUTTON_ID) || document.getElementById(SUBTITLE_BUTTON_ID)) { debugLog("Buttons were added concurrently, skipping."); return; } const summaryButton = document.createElement('button'); summaryButton.id = SUMMARY_BUTTON_ID; summaryButton.textContent = '📝 Gemini摘要'; Object.assign(summaryButton.style, { backgroundColor: '#1a73e8', color: 'white', border: 'none', borderRadius: '18px', padding: '0 16px', margin: '0 8px', cursor: 'pointer', fontWeight: '500', height: '36px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px', zIndex: '100', whiteSpace: 'nowrap', transition: 'background-color 0.3s ease' }); summaryButton.onmouseover = () => summaryButton.style.backgroundColor = '#185abc'; summaryButton.onmouseout = () => summaryButton.style.backgroundColor = '#1a73e8'; summaryButton.addEventListener('click', handleSummarizeClick); const subtitleButton = document.createElement('button'); subtitleButton.id = SUBTITLE_BUTTON_ID; subtitleButton.textContent = '🎯 生成字幕'; Object.assign(subtitleButton.style, { backgroundColor: '#28a745', color: 'white', border: 'none', borderRadius: '18px', padding: '0 16px', margin: '0 8px 0 0', cursor: 'pointer', fontWeight: '500', height: '36px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px', zIndex: '100', whiteSpace: 'nowrap', transition: 'background-color 0.3s ease' }); subtitleButton.onmouseover = () => subtitleButton.style.backgroundColor = '#218838'; subtitleButton.onmouseout = () => subtitleButton.style.backgroundColor = '#28a745'; subtitleButton.addEventListener('click', handleGenerateSubtitlesClick); if (anchorElement.id?.includes('subscribe-button') || anchorElement.tagName === 'BUTTON') { anchorElement.parentNode.insertBefore(summaryButton, anchorElement); anchorElement.parentNode.insertBefore(subtitleButton, summaryButton); debugLog(`Buttons inserted before anchor: ${anchorElement.id || anchorElement.tagName}`); } else if (anchorElement.id === 'actions' || anchorElement.id === 'end' || anchorElement.id === 'top-row') { anchorElement.insertBefore(summaryButton, anchorElement.firstChild); anchorElement.insertBefore(subtitleButton, summaryButton); debugLog(`Buttons inserted as first children of container: ${anchorElement.id || anchorElement.tagName}`); } else { anchorElement.appendChild(subtitleButton); anchorElement.appendChild(summaryButton); debugLog(`Buttons appended to container: ${anchorElement.id || anchorElement.tagName}`); } debugLog("Action buttons successfully added!"); } catch (error) { console.error('[YT->Gemini Optimized] Failed to add action buttons:', error); removeYouTubeActionButtonsIfExists(); } } function handleSummarizeClick() { try { const youtubeUrl = window.location.href; const titleElement = document.querySelector('h1.ytd-watch-metadata, #video-title, #title h1'); const videoTitle = titleElement?.textContent?.trim() || document.title.replace(/ - YouTube$/, '').trim() || 'Unknown Video'; const prompt = `请分析这个YouTube视频: ${youtubeUrl}\n\n提供一个全面的摘要,包括主要观点、关键见解和视频中讨论的重要细节,以结构化的方式分解内容,并包括任何重要的结论或要点。`; debugLog(`Summarize prompt: ${prompt}`); GM_setValue(PROMPT_KEY, prompt); GM_setValue(TITLE_KEY, videoTitle); GM_setValue(ORIGINAL_TITLE_KEY, videoTitle); GM_setValue(TIMESTAMP_KEY, Date.now()); GM_setValue(ACTION_TYPE_KEY, 'summary'); window.open('https://gemini.google.com/', '_blank'); showNotification(YOUTUBE_NOTIFICATION_ID, `已跳转到 Gemini 进行视频总结...\n"${videoTitle}"`, YOUTUBE_NOTIFICATION_STYLE); copyToClipboard(prompt); } catch (error) { console.error("[YT->Gemini Optimized] Error during summarize button click:", error); showNotification(YOUTUBE_NOTIFICATION_ID, `创建摘要时出错: ${error.message}`, { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025', color: 'white' }, 10000); } } async function handleGenerateSubtitlesClick() { try { await waitForElement(YOUTUBE_PLAYER_METADATA_SELECTOR, YOUTUBE_ELEMENT_TIMEOUT_MS); debugLog("Player metadata element found, proceeding to get video details for subtitles."); const youtubeUrl = window.location.href; const titleElement = document.querySelector('h1.ytd-watch-metadata, #video-title, #title h1'); const videoTitle = titleElement?.textContent?.trim() || document.title.replace(/ - YouTube$/, '').trim() || 'Unknown Video'; let videoDurationInSeconds = 0; try { const durationMetaElement = document.querySelector('meta[itemprop="duration"]'); if (durationMetaElement && durationMetaElement.content) { videoDurationInSeconds = parseISO8601DurationToSeconds(durationMetaElement.content); debugLog(`Video duration from meta: ${durationMetaElement.content} -> ${videoDurationInSeconds}s`); } else { debugLog("Duration meta tag not found or has no content."); } } catch (e) { debugLog("Failed to get video duration: " + e); } if (videoDurationInSeconds <= 0) { showNotification(YOUTUBE_NOTIFICATION_ID, "无法获取当前视频时长,无法启动字幕任务。请尝试刷新页面。", { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025' }, 15000); return; } const startTime = 0; const firstSegmentActualEndTimeSeconds = Math.min(videoDurationInSeconds, SUBTITLE_SEGMENT_DURATION_SECONDS); const startTimeFormatted = formatTimeHHMMSS(startTime); const endTimeFormatted = formatTimeHHMMSS(firstSegmentActualEndTimeSeconds); const prompt = `${youtubeUrl} 1.不要添加自己的语言 2.变成简体中文,流畅版本。 YouTube 请提取此视频从${startTimeFormatted}到${endTimeFormatted}的完整字幕文本。`; GM_setValue(PROMPT_KEY, prompt); const titleForGeminiNotificationDisplay = `${videoTitle} (字幕 ${startTimeFormatted}-${endTimeFormatted})`; GM_setValue(TITLE_KEY, titleForGeminiNotificationDisplay); GM_setValue(ORIGINAL_TITLE_KEY, videoTitle); GM_setValue(TIMESTAMP_KEY, Date.now()); GM_setValue(ACTION_TYPE_KEY, 'subtitle'); GM_setValue(VIDEO_TOTAL_DURATION_KEY, videoDurationInSeconds); GM_setValue(FIRST_SEGMENT_END_TIME_KEY, firstSegmentActualEndTimeSeconds); const youtubeNotificationMessage = `已跳转到 Gemini 生成字幕: ${startTimeFormatted} - ${endTimeFormatted}...\n"${videoTitle}"`; showNotification(YOUTUBE_NOTIFICATION_ID, youtubeNotificationMessage, YOUTUBE_NOTIFICATION_STYLE, 15000); window.open('https://gemini.google.com/', '_blank'); copyToClipboard(prompt); } catch (error) { console.error("[YT->Gemini] Error during subtitle click:", error); showNotification(YOUTUBE_NOTIFICATION_ID, `生成字幕时出错: ${error.message}`, { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025' }, 15000); } } function removeYouTubeActionButtonsIfExists() { const summaryButton = document.getElementById(SUMMARY_BUTTON_ID); if (summaryButton) { summaryButton.remove(); debugLog("Removed existing summary button."); } const subtitleButton = document.getElementById(SUBTITLE_BUTTON_ID); if (subtitleButton) { subtitleButton.remove(); debugLog("Removed existing subtitle button."); } } // --- Gemini Related --- const GEMINI_NOTIFICATION_ID = 'gemini-auto-notification'; const GEMINI_NOTIFICATION_STYLES = { info: { backgroundColor: '#e8f4fd', color: '#1967d2', border: '1px solid #a8c7fa' }, warning: { backgroundColor: '#fef7e0', color: '#a56300', border: '1px solid #fdd663' }, error: { backgroundColor: '#fce8e6', color: '#c5221f', border: '1px solid #f7a7a5' } }; const BASE_GEMINI_NOTIFICATION_STYLE = { position: 'fixed', bottom: '230px', right: '20px', padding: '15px 35px 15px 20px', borderRadius: '8px', zIndex: '9999', maxWidth: '350px', textAlign: 'left', boxSizing: 'border-box', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', whiteSpace: 'pre-wrap' }; function showGeminiNotification(message, type = "info", duration = 12000) { const style = { ...BASE_GEMINI_NOTIFICATION_STYLE, ...(GEMINI_NOTIFICATION_STYLES[type] || GEMINI_NOTIFICATION_STYLES.info) }; if (message.length > 150) { style.maxWidth = '500px'; } showNotification(GEMINI_NOTIFICATION_ID, message, style, duration); } async function handleGeminiPage() { debugLog("Gemini page loaded. Checking for pending action..."); const prompt = GM_getValue(PROMPT_KEY, null); const timestamp = GM_getValue(TIMESTAMP_KEY, 0); const notificationTitleForSimpleMessage = GM_getValue(TITLE_KEY, 'N/A'); const originalVideoTitle = GM_getValue(ORIGINAL_TITLE_KEY, 'N/A'); const actionType = GM_getValue(ACTION_TYPE_KEY, null); const videoTotalDurationSeconds = GM_getValue(VIDEO_TOTAL_DURATION_KEY, 0); if(actionType) GM_deleteValue(ACTION_TYPE_KEY); debugLog(`Retrieved from GM: actionType=${actionType}, promptExists=${!!prompt}, notificationTitleForSimpleMessage=${notificationTitleForSimpleMessage}, originalVideoTitle=${originalVideoTitle}, timestamp=${timestamp}, videoTotalDuration=${videoTotalDurationSeconds}`); const clearAllGmValues = () => { debugLog("Clearing all GM values."); GM_deleteValue(PROMPT_KEY); GM_deleteValue(TITLE_KEY); GM_deleteValue(ORIGINAL_TITLE_KEY); GM_deleteValue(TIMESTAMP_KEY); GM_deleteValue(VIDEO_TOTAL_DURATION_KEY); GM_deleteValue(FIRST_SEGMENT_END_TIME_KEY); }; if (!prompt || !actionType || Date.now() - timestamp > GEMINI_PROMPT_EXPIRY_MS) { debugLog("No valid prompt, actionType, or prompt expired."); clearAllGmValues(); return; } debugLog(`Valid action (${actionType}) found. Proceeding to interact with Gemini page.`); const initialNotificationMessageIntro = actionType === 'summary' ? '总结' : '字幕'; showGeminiNotification(`检测到来自 YouTube 的 "${initialNotificationMessageIntro}" 请求...\n视频: "${originalVideoTitle}"`, "info", 10000); const textareaSelectors = ['div.input-area > div.input-box > div[contenteditable="true"]', 'div[role="textbox"][contenteditable="true"]', 'textarea[aria-label*="Prompt"]', 'div[contenteditable="true"]', 'textarea']; const sendButtonSelectors = ['button[aria-label*="Send message"], button[aria-label*="发送消息"]', 'button:has(span[class*="send-icon"])', 'button.send-button', 'button:has(mat-icon[data-mat-icon-name="send"])', 'button[aria-label="Run"], button[aria-label="Submit"]']; const geminiSuccessNotificationDuration = 15000; try { debugLog("Waiting for textarea..."); const textarea = await waitForElement(textareaSelectors, GEMINI_ELEMENT_TIMEOUT_MS); debugLog("Textarea found. Focusing and inputting prompt."); textarea.focus(); if (textarea.isContentEditable) textarea.textContent = prompt; else if (textarea.tagName === 'TEXTAREA') textarea.value = prompt; else { debugLog("Cannot input text into the found element."); throw new Error("Could not determine how to input text into the found element."); } textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); debugLog("Prompt inserted and events dispatched."); await new Promise(resolve => setTimeout(resolve, 250)); debugLog("Waiting for send button..."); const sendButton = await waitForElement(sendButtonSelectors, GEMINI_ELEMENT_TIMEOUT_MS); debugLog("Send button found. Checking if enabled."); if (sendButton.disabled || sendButton.getAttribute('aria-disabled') === 'true') { debugLog("Send button is disabled. Waiting a bit longer..."); await new Promise(resolve => setTimeout(resolve, 600)); if (sendButton.disabled || sendButton.getAttribute('aria-disabled') === 'true') { debugLog("Send button remained disabled."); const errorMessage = "发送按钮仍然禁用。提示词已复制,请手动粘贴并发送。"; console.warn(`[YT->Gemini] ${errorMessage}`); showGeminiNotification(errorMessage, "warning", geminiSuccessNotificationDuration); copyToClipboard(prompt); clearAllGmValues(); return; } debugLog("Send button became enabled after waiting."); } debugLog("Clicking send button..."); sendButton.click(); debugLog("Prompt sent to Gemini successfully."); let finalNotificationMessage; const notificationMessageIntro = actionType === 'summary' ? '总结' : '字幕'; if (actionType === 'subtitle' && videoTotalDurationSeconds > SUBTITLE_SEGMENT_DURATION_SECONDS) { const firstSegmentDisplayEndTime = formatTimeHHMMSS(SUBTITLE_SEGMENT_DURATION_SECONDS); const suggestedNextStartTimeFormatted = firstSegmentDisplayEndTime; const suggestedNextSegmentEndTimeSeconds = Math.min(videoTotalDurationSeconds, SUBTITLE_SEGMENT_DURATION_SECONDS * 2); const suggestedNextEndTimeFormatted = formatTimeHHMMSS(suggestedNextSegmentEndTimeSeconds); finalNotificationMessage = `提示 如果您需要提取后续部分的字幕 (例如从 ${suggestedNextStartTimeFormatted} 到 ${suggestedNextEndTimeFormatted}): 1. 需要手动修改时间范围(最好不超过20分钟) 2. 若Gemini模型不是2.5 Pro,建议暂停切换 3. 若Gemini不生成或繁体字,让他“重做” `.trim(); } else { finalNotificationMessage = `"${notificationMessageIntro}" 请求已发送! (视频: "${notificationTitleForSimpleMessage}")`; } showGeminiNotification(finalNotificationMessage, "info", geminiSuccessNotificationDuration); clearAllGmValues(); } catch (error) { console.error('[YT->Gemini] Error on Gemini page:', error); showGeminiNotification(`自动操作失败: ${error.message}\n提示词已复制,请手动粘贴。`, "error", geminiSuccessNotificationDuration); copyToClipboard(prompt); clearAllGmValues(); } } // --- Main Execution Logic --- debugLog("Script starting..."); if (window.location.hostname.includes('www.youtube.com')) { debugLog("YouTube domain."); const runYouTubeLogic = async () => { if (isVideoPage()) { debugLog("On a video page. Proceeding with button logic."); try { await waitForElement(YOUTUBE_PLAYER_METADATA_SELECTOR, YOUTUBE_ELEMENT_TIMEOUT_MS); debugLog("YouTube key video elements ready for logic execution."); addYouTubeActionButtons(); } catch (error) { debugLog("Failed to find key YouTube video elements or other error on video page: " + error); removeYouTubeActionButtonsIfExists(); } } else { debugLog("Not on a video page. Ensuring action buttons are removed."); removeYouTubeActionButtonsIfExists(); } }; if (document.readyState === 'complete' || document.readyState === 'interactive') { setupThumbnailButtonObserver(); runYouTubeLogic(); } else { window.addEventListener('DOMContentLoaded', () => { setupThumbnailButtonObserver(); runYouTubeLogic(); }, { once: true }); } window.addEventListener('yt-navigate-finish', () => { debugLog("yt-navigate-finish event detected."); requestAnimationFrame(runYouTubeLogic); // Use requestAnimationFrame for smoother UI updates }); window.addEventListener('popstate', () => { debugLog("popstate event detected."); requestAnimationFrame(runYouTubeLogic); // Use requestAnimationFrame for smoother UI updates }); } else if (window.location.hostname.includes('gemini.google.com')) { debugLog("Gemini domain detected."); if (document.readyState === 'complete' || document.readyState === 'interactive') { handleGeminiPage(); } else { window.addEventListener('DOMContentLoaded', handleGeminiPage, { once: true }); } } else { debugLog(`Script loaded on unrecognized domain: ${window.location.hostname}`); } GM_addStyle(` #gemini-popup { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 9999; width: 300px; display: none; } #gemini-popup .button { width: 100%; padding: 10px; margin: 5px 0; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; } #gemini-popup .button:hover { background-color: #45a049; } #gemini-popup .status { margin-top: 10px; padding: 10px; border-radius: 4px; display: none; } #gemini-popup .success { background-color: #dff0d8; color: #3c763d; } #gemini-popup .error { background-color: #f2dede; color: #a94442; } `); function createPopup() { const popup = document.createElement('div'); popup.id = 'gemini-popup'; popup.innerHTML = `
`; document.body.appendChild(popup); return popup; } function showPopup() { const popup = document.getElementById('gemini-popup') || createPopup(); popup.style.display = 'block'; const startButton = document.getElementById('gemini-start-summary'); const statusDiv = document.getElementById('gemini-status'); startButton.onclick = () => { try { if (!isVideoPage()) { showStatus('请在YouTube视频页面使用此功能', 'error'); return; } handleSummarizeClick(); showStatus('开始总结视频...', 'success'); popup.style.display = 'none'; } catch (error) { showStatus('发生错误:' + error.message, 'error'); } }; function showStatus(message, type) { statusDiv.textContent = message; statusDiv.className = 'status ' + type; statusDiv.style.display = 'block'; } } function addExtensionIconClickListener() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { // This selector might need adjustment depending on the browser/extensions // It's a generic attempt to find an extension icon area. const extensionIcon = node.querySelector('ytd-masthead #buttons ytd-button-renderer'); if (extensionIcon) { extensionIcon.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showPopup(); }); } } } } } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } if (document.readyState === 'complete' || document.readyState === 'interactive') { addExtensionIconClickListener(); } else { window.addEventListener('DOMContentLoaded', addExtensionIconClickListener, {once: true}); } })();