// ==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);
}
}
})();