YouTube to Gemini 自动总结与字幕

在YouTube视频中添加按钮,点击后跳转到Gemini并自动输入提示词总结视频或生成字幕。长视频字幕首段自动处理,后续手动。

目前为 2025-05-10 提交的版本。查看 最新版本

// ==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
// ==/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<Element>} 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 = `
            <button id="gemini-start-summary" class="button">开始总结当前视频</button>
            <div id="gemini-status" class="status"></div>
        `;
        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});
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址