您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
YouTube 首页/搜索分段缩略图网格100%修复,Gemini一键总结/字幕 (性能优化版)
// ==UserScript== // @name YouTube to Gemini 自动总结与字幕 (优化版) // @namespace http://tampermonkey.net/ // @version 2.3 // @description YouTube 首页/搜索分段缩略图网格100%修复,Gemini一键总结/字幕 (性能优化版) // @author hengyu (优化 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 // ==/UserScript== (function() { 'use strict'; // --- 性能优化变量 --- let debounceTimer = null; let lastProcessedCount = 0; // 修复问题2:使用Map替代WeakSet,可以清理和重新处理 const processedElements = new Map(); // key: element, value: {videoId, timestamp} const ELEMENT_CACHE_TIME = 60000; // 1分钟后允许重新处理 // --- 终极分段网格修复 CSS --- // 只对首页和搜索结果页面应用网格布局修复 GM_addStyle(` /* 首页和搜索页面网格布局 */ body[data-is-home-page="true"] ytd-rich-grid-renderer > #contents, body[data-page-subtype="home"] ytd-rich-grid-renderer > #contents, body[data-page-type="search"] ytd-rich-grid-renderer > #contents { display: grid !important; grid-template-columns: repeat(2, 1fr) !important; gap: 24px 16px !important; width: 100% !important; margin: 0 auto !important; --ytd-rich-grid-items-per-row: 2 !important; --ytd-rich-grid-max-width: none !important; } @media (min-width: 1000px) { body[data-is-home-page="true"] ytd-rich-grid-renderer > #contents, body[data-page-subtype="home"] ytd-rich-grid-renderer > #contents, body[data-page-type="search"] ytd-rich-grid-renderer > #contents { grid-template-columns: repeat(3, 1fr) !important; --ytd-rich-grid-items-per-row: 3 !important; } } @media (min-width: 1400px) { body[data-is-home-page="true"] ytd-rich-grid-renderer > #contents, body[data-page-subtype="home"] ytd-rich-grid-renderer > #contents, body[data-page-type="search"] ytd-rich-grid-renderer > #contents { grid-template-columns: repeat(4, 1fr) !important; --ytd-rich-grid-items-per-row: 4 !important; } } @media (min-width: 1700px) { body[data-is-home-page="true"] ytd-rich-grid-renderer > #contents, body[data-page-subtype="home"] ytd-rich-grid-renderer > #contents, body[data-page-type="search"] ytd-rich-grid-renderer > #contents { grid-template-columns: repeat(5, 1fr) !important; --ytd-rich-grid-items-per-row: 5 !important; } } /* 确保只在首页和搜索页面修改布局结构 */ body[data-is-home-page="true"] ytd-rich-grid-row, body[data-is-home-page="true"] ytd-rich-grid-row > #contents, body[data-is-home-page="true"] ytd-rich-grid-row > #dismissible, body[data-is-home-page="true"] ytd-rich-grid-row > div, body[data-is-home-page="true"] ytd-rich-grid-row > #dismissible > #contents, body[data-is-home-page="true"] ytd-rich-grid-row > div > #contents, body[data-is-home-page="true"] ytd-rich-grid-row > div > #dismissible, body[data-is-home-page="true"] ytd-rich-grid-row > div > #dismissible > #contents, body[data-is-home-page="true"] ytd-rich-grid-row > .ytd-rich-grid-row, body[data-is-home-page="true"] ytd-rich-grid-row > div > .ytd-rich-grid-row, body[data-is-home-page="true"] ytd-rich-grid-row > div > div, body[data-is-home-page="true"] ytd-rich-grid-row > div > div > #contents, body[data-page-subtype="home"] ytd-rich-grid-row, body[data-page-subtype="home"] ytd-rich-grid-row > #contents, body[data-page-subtype="home"] ytd-rich-grid-row > #dismissible, body[data-page-subtype="home"] ytd-rich-grid-row > div, body[data-page-subtype="home"] ytd-rich-grid-row > #dismissible > #contents, body[data-page-subtype="home"] ytd-rich-grid-row > div > #contents, body[data-page-subtype="home"] ytd-rich-grid-row > div > #dismissible, body[data-page-subtype="home"] ytd-rich-grid-row > div > #dismissible > #contents, body[data-page-subtype="home"] ytd-rich-grid-row > .ytd-rich-grid-row, body[data-page-subtype="home"] ytd-rich-grid-row > div > .ytd-rich-grid-row, body[data-page-subtype="home"] ytd-rich-grid-row > div > div, body[data-page-subtype="home"] ytd-rich-grid-row > div > div > #contents, body[data-page-type="search"] ytd-rich-grid-row, body[data-page-type="search"] ytd-rich-grid-row > #contents, body[data-page-type="search"] ytd-rich-grid-row > #dismissible, body[data-page-type="search"] ytd-rich-grid-row > div, body[data-page-type="search"] ytd-rich-grid-row > #dismissible > #contents, body[data-page-type="search"] ytd-rich-grid-row > div > #contents, body[data-page-type="search"] ytd-rich-grid-row > div > #dismissible, body[data-page-type="search"] ytd-rich-grid-row > div > #dismissible > #contents, body[data-page-type="search"] ytd-rich-grid-row > .ytd-rich-grid-row, body[data-page-type="search"] ytd-rich-grid-row > div > .ytd-rich-grid-row, body[data-page-type="search"] ytd-rich-grid-row > div > div, body[data-page-type="search"] ytd-rich-grid-row > div > div > #contents { display: contents !important; } /* 视频项修复 - 仅限首页和搜索页面 */ body[data-is-home-page="true"] ytd-rich-item-renderer, body[data-is-home-page="true"] ytd-grid-video-renderer, body[data-is-home-page="true"] ytd-rich-grid-media, body[data-page-subtype="home"] ytd-rich-item-renderer, body[data-page-subtype="home"] ytd-grid-video-renderer, body[data-page-subtype="home"] ytd-rich-grid-media, body[data-page-type="search"] ytd-rich-item-renderer, body[data-page-type="search"] ytd-grid-video-renderer, body[data-page-type="search"] ytd-rich-grid-media { width: 100% !important; max-width: none !important; min-width: 0 !important; margin: 0 !important; box-sizing: border-box !important; } /* 弹性项 - 仅首页和搜索页面 */ body[data-is-home-page="true"] ytd-rich-grid-renderer > #contents > ytd-rich-section-renderer, body[data-is-home-page="true"] ytd-rich-grid-renderer > #contents > ytd-reel-shelf-renderer, body[data-page-subtype="home"] ytd-rich-grid-renderer > #contents > ytd-rich-section-renderer, body[data-page-subtype="home"] ytd-rich-grid-renderer > #contents > ytd-reel-shelf-renderer, body[data-page-type="search"] ytd-rich-grid-renderer > #contents > ytd-rich-section-renderer, body[data-page-type="search"] ytd-rich-grid-renderer > #contents > ytd-reel-shelf-renderer { grid-column: 1 / -1 !important; width: 100% !important; margin: 16px 0 !important; } /* 搜索页面修复 */ ytd-search ytd-video-renderer { display: block !important; position: relative !important; z-index: 1 !important; } ytd-search ytd-thumbnail { position: relative !important; z-index: 5 !important; } `); // --- Gemini 按钮与交互 --- const PROMPT_KEY = 'geminiPrompt'; const TITLE_KEY = 'videoTitle'; const ORIGINAL_TITLE_KEY = 'geminiOriginalVideoTitle'; const TIMESTAMP_KEY = 'timestamp'; const ACTION_TYPE_KEY = 'geminiActionType'; const VIDEO_TOTAL_DURATION_KEY = 'geminiVideoTotalDuration'; const FIRST_SEGMENT_END_TIME_KEY = 'geminiFirstSegmentEndTime'; const SUMMARY_BUTTON_ID = 'gemini-summarize-btn'; const SUBTITLE_BUTTON_ID = 'gemini-subtitle-btn'; const THUMBNAIL_BUTTON_CLASS = 'gemini-thumbnail-btn'; const THUMBNAIL_PROCESSED_FLAG = 'data-gemini-processed'; const YOUTUBE_NOTIFICATION_ID = 'gemini-yt-notification'; const YOUTUBE_CONFIRMATION_ID = 'gemini-yt-confirmation'; // 修复问题3:添加唯一会话ID const SESSION_ID_KEY = 'geminiSessionId'; // 恢复原始通知样式 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: '99999', maxWidth: 'calc(100% - 40px)', textAlign: 'left', boxSizing: 'border-box', whiteSpace: 'pre-wrap', boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }; const YOUTUBE_CONFIRMATION_STYLE = { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: 'rgba(33, 33, 33, 0.95)', color: 'white', padding: '20px 25px', borderRadius: '12px', zIndex: '999999', maxWidth: 'calc(100% - 60px)', minWidth: '300px', boxSizing: 'border-box', boxShadow: '0 8px 24px rgba(0,0,0,0.5)', display: 'flex', flexDirection: 'column', gap: '15px' }; // 缩略图按钮样式 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: 9999; display: flex; align-items: center; opacity: 0; transition: opacity 0.2s ease; pointer-events: auto !important; } #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}, ytd-playlist-video-renderer:hover .${THUMBNAIL_BUTTON_CLASS}, ytd-reel-item-renderer:hover .${THUMBNAIL_BUTTON_CLASS}, ytd-search ytd-video-renderer:hover .${THUMBNAIL_BUTTON_CLASS} { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; } .${THUMBNAIL_BUTTON_CLASS}:hover { background-color: rgba(0, 0, 0, 0.9); opacity: 1 !important; visibility: visible !important; } /* 搜索页面视频预览时的特殊处理 */ ytd-search .ytp-inline-preview-scrim ~ .${THUMBNAIL_BUTTON_CLASS}, ytd-search video ~ .${THUMBNAIL_BUTTON_CLASS}, ytd-search .html5-video-player ~ .${THUMBNAIL_BUTTON_CLASS} { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; z-index: 99999 !important; } /* 确保搜索页面的按钮在视频预览时仍然可见 */ ytd-search ytd-video-renderer:has(video) .${THUMBNAIL_BUTTON_CLASS}, ytd-search ytd-video-renderer:has(.ytp-inline-preview-scrim) .${THUMBNAIL_BUTTON_CLASS} { opacity: 1 !important; z-index: 99999 !important; } .gemini-confirmation-btn { padding: 8px 20px; border-radius: 4px; border: none; cursor: pointer; font-weight: 500; font-size: 14px; transition: background-color 0.2s ease; } .gemini-confirmation-confirm { background-color: #1a73e8; color: white; } .gemini-confirmation-confirm:hover { background-color: #0d65d9; } .gemini-confirmation-cancel { background-color: #5f6368; color: white; margin-right: 10px; } .gemini-confirmation-cancel:hover { background-color: #494c50; } `); // 辅助函数 function showNotification(elementId, message, styles, duration = 15000) { let existing = document.getElementById(elementId); if (existing) { clearTimeout(parseInt(existing.dataset.timeoutId)); existing.remove(); } const notif = document.createElement('div'); notif.id = elementId; notif.textContent = message; Object.assign(notif.style, styles); document.body.appendChild(notif); const btn = document.createElement('button'); btn.textContent = '✕'; Object.assign(btn.style, { position: 'absolute', top: '5px', right: '10px', background: 'transparent', border: 'none', color: 'inherit', fontSize: '16px', cursor: 'pointer', padding: '0', lineHeight: '1' }); btn.onclick = () => notif.remove(); notif.appendChild(btn); notif.dataset.timeoutId = setTimeout(() => notif.remove(), duration).toString(); return notif; } function showConfirmation(elementId, title, message, videoInfo, onConfirm, onCancel, styles) { let existing = document.getElementById(elementId); if (existing) existing.remove(); const dialog = document.createElement('div'); dialog.id = elementId; Object.assign(dialog.style, styles); document.body.appendChild(dialog); const titleElem = document.createElement('h3'); titleElem.textContent = title; titleElem.style.margin = '0 0 10px 0'; titleElem.style.fontSize = '18px'; const messageElem = document.createElement('div'); messageElem.textContent = message; messageElem.style.marginBottom = '15px'; messageElem.style.fontSize = '14px'; const videoTitleElem = document.createElement('div'); videoTitleElem.textContent = `视频标题: ${videoInfo.title}`; videoTitleElem.style.marginBottom = '5px'; videoTitleElem.style.fontWeight = 'bold'; const videoIdElem = document.createElement('div'); videoIdElem.textContent = `视频ID: ${videoInfo.id}`; videoIdElem.style.fontSize = '12px'; videoIdElem.style.color = '#aaa'; videoIdElem.style.marginBottom = '15px'; const buttonsContainer = document.createElement('div'); buttonsContainer.style.display = 'flex'; buttonsContainer.style.justifyContent = 'flex-end'; buttonsContainer.style.gap = '10px'; const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.className = 'gemini-confirmation-btn gemini-confirmation-cancel'; cancelBtn.onclick = () => { dialog.remove(); if (onCancel) onCancel(); }; const confirmBtn = document.createElement('button'); confirmBtn.textContent = '确认'; confirmBtn.className = 'gemini-confirmation-btn gemini-confirmation-confirm'; confirmBtn.onclick = () => { dialog.remove(); if (onConfirm) onConfirm(videoInfo); }; buttonsContainer.appendChild(cancelBtn); buttonsContainer.appendChild(confirmBtn); dialog.appendChild(titleElem); dialog.appendChild(messageElem); dialog.appendChild(videoTitleElem); dialog.appendChild(videoIdElem); dialog.appendChild(buttonsContainer); return dialog; } function copyToClipboard(text) { navigator.clipboard.writeText(text).catch(() => { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch {} document.body.removeChild(ta); }); } function isVideoPage() { return window.location.pathname === '/watch' && new URLSearchParams(window.location.search).has('v'); } // 验证YouTube视频ID格式 function isValidYouTubeVideoId(id) { return id && typeof id === 'string' && /^[A-Za-z0-9_-]{11}$/.test(id); } // 生成唯一会话ID function generateSessionId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } // --- 优化后的视频信息提取函数 --- function getVideoInfoFromElement(element) { // 修复问题2:检查缓存是否过期 const cached = processedElements.get(element); if (cached && (Date.now() - cached.timestamp < ELEMENT_CACHE_TIME)) { return null; // 仍在缓存期内 } let videoId = ''; let videoTitle = ''; // 优化:优先检查最可能的数据属性 const possibleIdSources = [ () => element.dataset?.videoId, () => element.getAttribute('video-id'), () => { // 优化的链接查找 - 使用更精确的选择器 const link = element.querySelector('a[href*="/watch?v="]:first-child'); if (link) { const match = link.href.match(/\/watch\?v=([^&]+)/); return match?.[1]; } }, () => { // 优化的缩略图查找 const img = element.querySelector('img[src*="/vi/"]:first-child, img[src*="i.ytimg.com"]:first-child'); if (img) { const match = img.src.match(/\/vi\/([^\/]+)\//) || img.src.match(/\/([A-Za-z0-9_-]{11})\/[\w]+\.jpg/); return match?.[1]; } } ]; // 按优先级尝试获取视频ID for (const getSource of possibleIdSources) { const id = getSource(); if (isValidYouTubeVideoId(id)) { videoId = id; break; } } // 优化的标题提取 - 按优先级排序 const titleSelectors = [ '#video-title', 'h3 a[title]', '.title[title]', 'yt-formatted-string[title]', 'span[title]' ]; for (const selector of titleSelectors) { const titleElement = element.querySelector(selector); if (titleElement) { const possibleTitle = titleElement.textContent?.trim() || titleElement.getAttribute('title')?.trim(); if (possibleTitle && possibleTitle.length > 5) { videoTitle = possibleTitle; break; } } } // 验证结果 if (!isValidYouTubeVideoId(videoId) || !videoTitle) { return null; } // 更新缓存 processedElements.set(element, { videoId: videoId, timestamp: Date.now() }); return { id: videoId, title: videoTitle, url: `https://www.youtube.com/watch?v=${videoId}` }; } function processVideoSummary(videoInfo) { const prompt = `请分析这个YouTube视频: ${videoInfo.url}\n\n提供一个全面的摘要,包括主要观点、关键见解和视频中讨论的重要细节,以结构化的方式分解内容,并包括任何重要的结论或要点。`; // 修复问题3:生成唯一会话ID const sessionId = generateSessionId(); 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'); GM_setValue(SESSION_ID_KEY, sessionId); window.open('https://gemini.google.com/', '_blank'); showNotification( YOUTUBE_NOTIFICATION_ID, `已跳转到 Gemini!\n系统将尝试自动输入提示词并发送请求。\n\n视频: "${videoInfo.title}"\n\n(如果自动操作失败,提示词已复制到剪贴板,请手动粘贴)`, YOUTUBE_NOTIFICATION_STYLE, 10000 ); copyToClipboard(prompt); } function handleThumbnailButtonClick(event, videoInfo) { if (event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); if (event.cancelable) event.returnValue = false; } if (!videoInfo || !videoInfo.url || !videoInfo.title) { showNotification( YOUTUBE_NOTIFICATION_ID, "无法获取视频信息,请尝试直接在视频页面使用总结功能。", { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025' }, 5000 ); return false; } if (!isValidYouTubeVideoId(videoInfo.id)) { showNotification( YOUTUBE_NOTIFICATION_ID, `获取到的视频ID格式无效: ${videoInfo.id}\n请尝试直接在视频页面使用总结功能。`, { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025' }, 5000 ); return false; } showConfirmation( YOUTUBE_CONFIRMATION_ID, "确认视频信息", "请确认以下视频信息是否正确:", videoInfo, processVideoSummary, null, YOUTUBE_CONFIRMATION_STYLE ); return false; } // --- 优化后的缩略图按钮添加函数 --- function addThumbnailButtons() { if (isVideoPage()) return; const isSearchPage = window.location.pathname === '/results'; // 优化:使用更精确的选择器,减少误匹配 const videoElementSelectors = isSearchPage ? [ 'ytd-search ytd-video-renderer:has(ytd-thumbnail)', 'ytd-video-renderer:has(#thumbnail)' ] : [ 'ytd-rich-item-renderer:has(ytd-thumbnail)', 'ytd-grid-video-renderer:has(#thumbnail)', 'ytd-compact-video-renderer:has(ytd-thumbnail)', 'ytd-playlist-video-renderer:has(ytd-thumbnail)' ]; let processedCount = 0; const elements = document.querySelectorAll(videoElementSelectors.join(',')); // 优化:如果元素数量没有变化,且已经处理过相同数量,则跳过 if (elements.length === lastProcessedCount && elements.length > 0) { // 快速验证是否所有元素都已处理 let allProcessed = true; for (const element of elements) { if (!element.hasAttribute(THUMBNAIL_PROCESSED_FLAG)) { allProcessed = false; break; } } if (allProcessed) return; } elements.forEach(element => { // 优化:更快的已处理检查 if (element.hasAttribute(THUMBNAIL_PROCESSED_FLAG)) { processedCount++; return; } // 优化:更精确的缩略图容器查找 let thumbnailContainer = element.querySelector('ytd-thumbnail a, #thumbnail'); 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总结此视频'; // 优化:简化事件处理 const eventHandler = (e) => { if (e.type === 'click') { return handleThumbnailButtonClick(e, videoInfo); } e.stopPropagation(); e.preventDefault(); return false; }; button.addEventListener('click', eventHandler, { capture: true, passive: false }); button.addEventListener('mousedown', eventHandler, { capture: true, passive: false }); // 搜索页面特殊处理 if (isSearchPage) { // 监听视频预览 const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && (node.tagName === 'VIDEO' || node.classList?.contains('ytp-inline-preview-scrim') || node.classList?.contains('html5-video-player'))) { // 强制显示按钮 button.style.opacity = '1'; button.style.zIndex = '99999'; button.style.pointerEvents = 'auto'; button.style.visibility = 'visible'; } } } } }); observer.observe(element, { childList: true, subtree: true }); // 保存observer引用以便清理 button._observer = observer; } // 确保容器有相对定位 if (getComputedStyle(thumbnailContainer).position === 'static') { thumbnailContainer.style.position = 'relative'; } thumbnailContainer.appendChild(button); element.setAttribute(THUMBNAIL_PROCESSED_FLAG, 'true'); processedCount++; }); lastProcessedCount = elements.length; } // --- 优化后的智能防抖函数 --- function debouncedAddThumbnailButtons() { if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(() => { addThumbnailButtons(); debounceTimer = null; }, 200); // 200ms防抖,平衡响应性和性能 } // --- 优化后的缩略图按钮系统设置 --- function setupThumbnailButtonSystem() { // 立即执行一次 addThumbnailButtons(); // 优化:使用防抖的MutationObserver const obs = new MutationObserver(() => { // 只在非视频页面执行 if (!isVideoPage()) { debouncedAddThumbnailButtons(); } }); obs.observe(document.body, { childList: true, subtree: true, // 优化:只观察必要的属性变化 attributes: false, attributeOldValue: false, characterData: false, characterDataOldValue: false }); // 优化:保留setInterval作为备用,但频率降低 setInterval(() => { if (!isVideoPage()) { // 只在元素数量发生变化时才执行 const currentElementCount = document.querySelectorAll('ytd-rich-item-renderer, ytd-video-renderer').length; if (currentElementCount !== lastProcessedCount) { addThumbnailButtons(); } } }, 3000); // 从1500ms增加到3000ms // 页面加载完成后执行 if (document.readyState === 'complete') { setTimeout(addThumbnailButtons, 800); } else { window.addEventListener('load', () => setTimeout(addThumbnailButtons, 800), { once: true }); } } // --- 视频页面按钮功能 (修复容器选择) --- function addYouTubeActionButtons() { if (!isVideoPage()) { removeYouTubeActionButtonsIfExists(); return; } if (document.getElementById(SUMMARY_BUTTON_ID) || document.getElementById(SUBTITLE_BUTTON_ID)) return; // 修复:使用更可靠的容器选择逻辑 const container = document.querySelector('ytd-masthead #end') || document.querySelector('ytd-masthead #buttons') || document.querySelector('ytd-masthead .ytd-masthead-right') || document.querySelector('#masthead-container #end') || document.querySelector('#container.ytd-masthead #end') || document.querySelector('ytd-masthead'); if (!container) { console.log('YouTube Gemini Script: 无法找到合适的容器来放置按钮'); return; } const buttonsWrapper = document.createElement('div'); buttonsWrapper.style.display = 'inline-flex'; buttonsWrapper.style.alignItems = 'center'; buttonsWrapper.style.marginRight = '16px'; 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: 'all 0.2s ease' }); 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', cursor: 'pointer', fontWeight: '500', height: '36px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px', zIndex: '100', whiteSpace: 'nowrap', transition: 'all 0.2s ease' }); const mediaQuery = window.matchMedia('(max-width: 768px)'); const adjustForMobile = () => { if (mediaQuery.matches) { subtitleButton.style.fontSize = '12px'; subtitleButton.style.padding = '0 10px'; subtitleButton.style.height = '32px'; summaryButton.style.fontSize = '12px'; summaryButton.style.padding = '0 10px'; summaryButton.style.height = '32px'; } else { subtitleButton.style.fontSize = '14px'; subtitleButton.style.padding = '0 16px'; subtitleButton.style.height = '36px'; summaryButton.style.fontSize = '14px'; summaryButton.style.padding = '0 16px'; summaryButton.style.height = '36px'; } }; mediaQuery.addEventListener('change', adjustForMobile); adjustForMobile(); subtitleButton.addEventListener('click', handleGenerateSubtitlesClick); summaryButton.addEventListener('click', handleSummarizeClick); buttonsWrapper.appendChild(subtitleButton); buttonsWrapper.appendChild(summaryButton); // 修复:改进插入逻辑,提供更多备选位置 const insertTargets = [ container.querySelector('#create-icon'), container.querySelector('button[aria-label*="创建"]'), container.querySelector('button[aria-label*="Create"]'), container.querySelector('#avatar-btn'), container.querySelector('ytd-notification-topbar-button-renderer'), container.querySelector('.ytd-masthead-right') ]; let inserted = false; for (const target of insertTargets) { if (target) { container.insertBefore(buttonsWrapper, target); inserted = true; console.log('YouTube Gemini Script: 按钮已成功插入到', target); break; } } // 如果没有找到合适的插入位置,就插入到容器的开头 if (!inserted) { if (container.firstChild) { container.insertBefore(buttonsWrapper, container.firstChild); } else { container.appendChild(buttonsWrapper); } console.log('YouTube Gemini Script: 按钮已插入到容器的默认位置'); } } function handleSummarizeClick() { const youtubeUrl = window.location.href; const urlParams = new URLSearchParams(window.location.search); const videoId = urlParams.get('v'); if (!isValidYouTubeVideoId(videoId)) { showNotification( YOUTUBE_NOTIFICATION_ID, "无法获取有效的视频ID,请确认当前是否在YouTube视频页面。", { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025' }, 5000 ); return; } const titleSelectors = [ 'h1.ytd-watch-metadata', '#video-title', '#title h1', '.title', 'yt-formatted-string.ytd-watch-metadata' ]; let videoTitle = ''; for (const selector of titleSelectors) { const titleElement = document.querySelector(selector); if (titleElement) { videoTitle = titleElement.textContent?.trim(); if (videoTitle) break; } } if (!videoTitle) { videoTitle = document.title.replace(/ - YouTube$/, '').trim() || 'Unknown Video'; } const videoInfo = { id: videoId, title: videoTitle, url: youtubeUrl }; processVideoSummary(videoInfo); } function handleGenerateSubtitlesClick() { const youtubeUrl = window.location.href; const titleElement = document.querySelector('h1.ytd-watch-metadata, #video-title, #title h1, .title'); const videoTitle = titleElement?.textContent?.trim() || document.title.replace(/ - YouTube$/, '').trim() || 'Unknown Video'; let videoDurationInSeconds = 0; const durationMeta = document.querySelector('meta[itemprop="duration"]'); if (durationMeta?.content) { const match = durationMeta.content.match(/PT(\d+H)?(\d+M)?(\d+S)?/); if (match) { videoDurationInSeconds = 0; if (match[1]) videoDurationInSeconds += parseInt(match[1].replace('H', '')) * 3600; if (match[2]) videoDurationInSeconds += parseInt(match[2].replace('M', '')) * 60; if (match[3]) videoDurationInSeconds += parseInt(match[3].replace('S', '')); } } if (videoDurationInSeconds <= 0) { showNotification(YOUTUBE_NOTIFICATION_ID, "无法获取视频时长,无法启动字幕任务。", { ...YOUTUBE_NOTIFICATION_STYLE, backgroundColor: '#d93025' }, 15000); return; } const firstSegmentEnd = Math.min(videoDurationInSeconds, 1200); const prompt = `${youtubeUrl}\n1.不要添加自己的语言\n2.变成简体中文,流畅版本。\n\nYouTube\n请提取此视频从00:00:00到${new Date(firstSegmentEnd * 1000).toISOString().substr(11, 8)}的完整字幕文本。`; // 修复问题3:生成唯一会话ID const sessionId = generateSessionId(); GM_setValue(PROMPT_KEY, prompt); GM_setValue(TITLE_KEY, `${videoTitle} (字幕 00:00:00-${new Date(firstSegmentEnd * 1000).toISOString().substr(11, 8)})`); 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, firstSegmentEnd); GM_setValue(SESSION_ID_KEY, sessionId); showNotification(YOUTUBE_NOTIFICATION_ID, `已跳转到 Gemini 生成字幕: 00:00:00 - ${new Date(firstSegmentEnd * 1000).toISOString().substr(11, 8)}...\n"${videoTitle}"`, YOUTUBE_NOTIFICATION_STYLE, 15000); window.open('https://gemini.google.com/', '_blank'); copyToClipboard(prompt); } function removeYouTubeActionButtonsIfExists() { [SUMMARY_BUTTON_ID, SUBTITLE_BUTTON_ID].forEach(id => { const button = document.getElementById(id); if (button) button.remove(); }); } // --- 页面类型检测函数 --- function detectYouTubePageType() { if (!document.body) return; let isHomePage = window.location.pathname === '/' || window.location.pathname === '/feed/subscriptions'; let isChannelPage = window.location.pathname.includes('/channel/') || window.location.pathname.includes('/c/') || window.location.pathname.includes('/user/') || window.location.pathname.includes('/@'); let isSearchPage = window.location.pathname === '/results'; if (isHomePage) { document.body.setAttribute('data-is-home-page', 'true'); document.body.setAttribute('data-page-subtype', 'home'); } else { document.body.removeAttribute('data-is-home-page'); } if (isChannelPage) { document.body.setAttribute('data-page-subtype', 'channels'); } else if (isSearchPage) { document.body.setAttribute('data-page-type', 'search'); } } // 修复问题1:添加更可靠的URL变化检测 function setupUrlChangeDetection() { let lastUrl = location.href; // 监听popstate事件(浏览器前进/后退) window.addEventListener('popstate', function() { if (location.href !== lastUrl) { lastUrl = location.href; handleUrlChange(); } }); // 监听YouTube的导航事件 document.addEventListener('yt-navigate-finish', function() { if (location.href !== lastUrl) { lastUrl = location.href; handleUrlChange(); } }); // 备用:仍然保留MutationObserver const urlObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; handleUrlChange(); } }); urlObserver.observe(document, { subtree: true, childList: true }); // 处理URL变化的函数 function handleUrlChange() { // 清理缓存和observers processedElements.forEach((value, element) => { const button = element.querySelector(`.${THUMBNAIL_BUTTON_CLASS}`); if (button?._observer) { button._observer.disconnect(); } }); processedElements.clear(); lastProcessedCount = 0; setTimeout(() => { detectYouTubePageType(); if (isVideoPage()) { addYouTubeActionButtons(); } else { removeYouTubeActionButtonsIfExists(); } }, 800); } } // --- 页面初始化 (优化版) --- if (window.location.hostname.includes('www.youtube.com')) { detectYouTubePageType(); // 修复问题1:使用新的URL检测系统 setupUrlChangeDetection(); if (document.readyState === 'complete' || document.readyState === 'interactive') { setupThumbnailButtonSystem(); setTimeout(addYouTubeActionButtons, 800); // 优化:减少检查频率 setInterval(() => { if (isVideoPage()) { if (!document.getElementById(SUMMARY_BUTTON_ID) || !document.getElementById(SUBTITLE_BUTTON_ID)) { console.log('YouTube Gemini Script: 检测到按钮缺失,尝试重新添加'); addYouTubeActionButtons(); } } detectYouTubePageType(); }, 8000); // 从5000ms增加到8000ms } else { document.addEventListener('DOMContentLoaded', () => { detectYouTubePageType(); setupThumbnailButtonSystem(); setTimeout(addYouTubeActionButtons, 800); }, { once: true }); } } else if (window.location.hostname.includes('gemini.google.com')) { // 修复问题3:增加会话ID验证 const prompt = GM_getValue(PROMPT_KEY); const timestamp = GM_getValue(TIMESTAMP_KEY, 0); const actionType = GM_getValue(ACTION_TYPE_KEY); const sessionId = GM_getValue(SESSION_ID_KEY); const referrerIsYouTube = document.referrer.includes('youtube.com'); // 增加更严格的验证条件 if (prompt && actionType && sessionId && Date.now() - timestamp <= 60000 && // 缩短到1分钟 referrerIsYouTube) { setTimeout(() => { const textarea = document.querySelector('textarea, div[contenteditable="true"]'); if (textarea) { if (textarea.isContentEditable) textarea.textContent = prompt; else textarea.value = prompt; textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); setTimeout(() => { const sendBtn = document.querySelector('button[aria-label*="Send"],button[aria-label*="发送"],button[aria-label*="提交"],button[aria-label*="Run"],button[aria-label*="Submit"]'); if (sendBtn && !sendBtn.disabled) { sendBtn.click(); // 立即清理,避免重复使用 setTimeout(() => { GM_deleteValue(PROMPT_KEY); GM_deleteValue(TITLE_KEY); GM_deleteValue(ORIGINAL_TITLE_KEY); GM_deleteValue(TIMESTAMP_KEY); GM_deleteValue(ACTION_TYPE_KEY); GM_deleteValue(VIDEO_TOTAL_DURATION_KEY); GM_deleteValue(FIRST_SEGMENT_END_TIME_KEY); GM_deleteValue(SESSION_ID_KEY); }, 1000); // 缩短清理时间 } }, 500); } }, 1200); } } })();