YouTube 到 Gemini 自动摘要生成器

在YouTube视频中添加按钮,点击后跳转到Gemini并自动输入提示词总结视频

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

// ==UserScript==
// @name         YouTube 到 Gemini 自动摘要生成器
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description  在YouTube视频中添加按钮,点击后跳转到Gemini并自动输入提示词总结视频
// @author       hengyu (Optimized by Assistant)
// @match        *://www.youtube.com/*
// @match        *://youtube.com/*
// @match        *://gemini.google.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置 ---
    const CHECK_INTERVAL_MS = 100; // 检查元素的频率(毫秒)
    const YOUTUBE_ELEMENT_TIMEOUT_MS = 10000; // 等待YouTube元素的最大时间(毫秒)
    const GEMINI_ELEMENT_TIMEOUT_MS = 15000; // 等待Gemini元素的最大时间(毫秒)
    const GEMINI_PROMPT_EXPIRY_MS = 300000; // 提示词传输有效期5分钟
    const URL_CHECK_INTERVAL_MS = 500; // URL变化检查频率

    // --- 调试日志 ---
    function debugLog(message) {
        console.log(`[YouTube to Gemini] ${message}`);
    }

    // --- 辅助函数 ---
    function waitForElement(selector, timeoutMs, parent = document) {
        return new Promise((resolve, reject) => {
            let element = parent.querySelector(selector);
            if (element && element.offsetWidth > 0 && element.offsetHeight > 0) {
                return resolve(element);
            }

            const intervalId = setInterval(() => {
                element = parent.querySelector(selector);
                if (element && element.offsetWidth > 0 && element.offsetHeight > 0) {
                    clearInterval(intervalId);
                    clearTimeout(timeoutId);
                    resolve(element);
                }
            }, CHECK_INTERVAL_MS);

            const timeoutId = setTimeout(() => {
                clearInterval(intervalId);
                debugLog(`Element not found or not visible after ${timeoutMs}ms: ${selector}`);
                reject(new Error(`Element not found or not visible: ${selector}`));
            }, timeoutMs);
        });
    }

    function waitForElements(selectors, timeoutMs, parent = document) {
        return new Promise((resolve, reject) => {
            let foundElement = null;
            const startTime = Date.now();

            function checkElements() {
                for (const selector of selectors) {
                    const elements = parent.querySelectorAll(selector);
                    for (const el of elements) {
                        if (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0) {
                            if (selectors.some(s => s.includes('button')) && el.disabled) {
                                continue; // 跳过禁用的按钮
                            }
                            foundElement = el;
                            break;
                        }
                    }
                    if (foundElement) break;
                }

                if (foundElement) {
                    clearInterval(intervalId);
                    clearTimeout(timeoutId);
                    resolve(foundElement);
                } else if (Date.now() - startTime > timeoutMs) {
                    clearInterval(intervalId);
                    debugLog(`Elements not found or not visible after ${timeoutMs}ms: ${selectors.join(', ')}`);
                    reject(new Error(`Elements not found or not visible: ${selectors.join(', ')}`));
                }
            }

            const intervalId = setInterval(checkElements, CHECK_INTERVAL_MS);
            const timeoutId = setTimeout(() => {
                clearInterval(intervalId);
                if (!foundElement) {
                    debugLog(`Elements not found or not visible after ${timeoutMs}ms: ${selectors.join(', ')}`);
                    reject(new Error(`Elements not found or not visible: ${selectors.join(', ')}`));
                }
            }, timeoutMs);

            // 初始检查
            checkElements();
        });
    }

    function copyToClipboard(text) {
        try {
            // 尝试使用现代剪贴板API
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(text)
                    .catch(err => {
                        debugLog(`Clipboard API失败: ${err},使用后备方法`);
                        legacyClipboardCopy(text);
                    });
            } else {
                legacyClipboardCopy(text);
            }
        } catch (e) {
            debugLog(`复制到剪贴板时出错: ${e}`);
            legacyClipboardCopy(text);
        }
    }

    function legacyClipboardCopy(text) {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        document.body.appendChild(textarea);
        textarea.select();
        try {
            document.execCommand('copy');
        } catch (err) {
            debugLog('Failed to copy to clipboard using execCommand.');
        }
        document.body.removeChild(textarea);
    }

    function showNotification(elementId, message, styles, duration = 15000) {
        // 移除已存在的通知
        let existingNotification = document.getElementById(elementId);
        if (existingNotification) {
            document.body.removeChild(existingNotification);
        }

        const notification = document.createElement('div');
        notification.id = elementId;
        notification.innerText = message;
        Object.assign(notification.style, styles); // 应用基础样式

        document.body.appendChild(notification);

        // 添加关闭按钮
        const closeButton = document.createElement('button');
        closeButton.innerText = '✕';
        closeButton.style.position = 'absolute';
        closeButton.style.top = '5px';
        closeButton.style.right = '10px';
        closeButton.style.background = 'transparent';
        closeButton.style.border = 'none';
        closeButton.style.color = 'inherit';
        closeButton.style.fontSize = '16px';
        closeButton.style.cursor = 'pointer';
        closeButton.onclick = function() {
            if (document.body.contains(notification)) {
                document.body.removeChild(notification);
            }
        };
        notification.appendChild(closeButton);

        // 自动移除
        const timeoutId = setTimeout(() => {
            if (document.body.contains(notification)) {
                document.body.removeChild(notification);
            }
        }, duration);
        notification.dataset.timeoutId = timeoutId;
    }

    // --- YouTube 相关函数 ---
    const YOUTUBE_NOTIFICATION_ID = 'gemini-yt-notification';
    const YOUTUBE_NOTIFICATION_STYLE = {
        position: 'fixed',
        bottom: '20px',
        left: '50%',
        transform: 'translateX(-50%)',
        backgroundColor: 'rgba(0,0,0,0.8)',
        color: 'white',
        padding: '20px',
        borderRadius: '8px',
        zIndex: '9999',
        maxWidth: '80%',
        textAlign: 'left',
        whiteSpace: 'pre-line'
    };

    // 检查是否为视频页面
    function isVideoPage() {
        return window.location.pathname === '/watch' && window.location.search.includes('?v=');
    }

    function addSummarizeButton() {
        // 检查是否是视频页面
        if (!isVideoPage()) {
            debugLog("不是视频页面,不添加按钮");
            return;
        }

        // 防止重复添加按钮
        if (document.getElementById('gemini-summarize-btn')) {
            debugLog("摘要按钮已存在");
            return;
        }

        debugLog("尝试添加摘要按钮...");

        // 尝试多个可能的容器选择器
        const containerSelectors = [
            '#masthead #end',
            '#top-row ytd-video-owner-renderer',
            '#above-the-fold #top-row',
            '#owner',
            'ytd-watch-metadata'
        ];

        // 尝试找到容器并添加按钮
        (async function() {
            for (const selector of containerSelectors) {
                try {
                    const container = await waitForElement(selector, 2000);
                    if (container) {
                        // 再次检查按钮是否存在
                        if (document.getElementById('gemini-summarize-btn')) {
                            return;
                        }

                        const button = document.createElement('button');
                        button.id = 'gemini-summarize-btn';
                        button.innerText = '📝 Gemini摘要';

                        // 应用样式
                        Object.assign(button.style, {
                            backgroundColor: '#2F80ED',
                            color: 'white',
                            border: 'none',
                            borderRadius: '4px',
                            padding: '8px 16px',
                            margin: '0 16px',
                            cursor: 'pointer',
                            fontWeight: 'bold',
                            height: '36px',
                            display: 'flex',
                            alignItems: 'center',
                            zIndex: '9999'
                        });

                        // 添加点击事件
                        button.addEventListener('click', function() {
                            try {
                                const youtubeUrl = window.location.href;
                                const videoTitle = document.querySelector('h1.ytd-watch-metadata')?.textContent?.trim()
                                              || document.title.replace(' - YouTube', '');

                                const prompt = `请分析这个YouTube视频: ${youtubeUrl}\n\n提供一个全面的摘要,包括主要观点、关键见解和视频中讨论的重要细节,以结构化的方式分解内容,并包括任何重要的结论或要点。`;

                                // 存储数据到GM变量
                                GM_setValue('geminiPrompt', prompt);
                                GM_setValue('videoTitle', videoTitle);
                                GM_setValue('timestamp', Date.now());

                                // 在新标签页打开Gemini
                                window.open('https://gemini.google.com/', '_blank');

                                // 显示通知
                                showNotification(YOUTUBE_NOTIFICATION_ID, `
已跳转到Gemini!
系统将尝试自动输入并发送提示。

如果自动操作失败,提示词已复制到剪贴板,您可以手动粘贴。

视频: "${videoTitle}"
                                `.trim(), YOUTUBE_NOTIFICATION_STYLE);

                                // 复制到剪贴板作为备份
                                copyToClipboard(prompt);
                            } catch (error) {
                                console.error("Button click error:", error);
                                alert("摘要功能出错: " + error.message);
                            }
                        });

                        // 添加按钮到容器
                        container.insertBefore(button, container.firstChild);
                        debugLog("摘要按钮成功添加!");
                        return; // 成功添加后退出
                    }
                } catch (e) {
                    // 继续尝试下一个选择器
                }
            }

            debugLog("所有选择器都尝试失败,无法添加按钮");
        })();
    }

    // --- Gemini 相关函数 ---
    const GEMINI_NOTIFICATION_ID = 'gemini-auto-notification';
    const GEMINI_NOTIFICATION_STYLES = {
        info: {
            backgroundColor: '#e8f4fd', color: '#0866c2', border: '1px solid #b8daff'
        },
        warning: {
            backgroundColor: '#fff3e0', color: '#b35d00', border: '1px solid #ffe0b2'
        },
        error: {
            backgroundColor: '#fdecea', color: '#c62828', border: '1px solid #ffcdd2'
        }
    };
    const BASE_GEMINI_NOTIFICATION_STYLE = {
        position: 'fixed', bottom: '20px', right: '20px', padding: '15px 20px',
        borderRadius: '8px', zIndex: '9999', maxWidth: '350px', textAlign: 'left',
        boxShadow: '0 4px 12px rgba(0,0,0,0.15)', whiteSpace: 'pre-line'
    };

    function showGeminiNotification(message, type = "info") {
        const style = { ...BASE_GEMINI_NOTIFICATION_STYLE, ...(GEMINI_NOTIFICATION_STYLES[type] || GEMINI_NOTIFICATION_STYLES.info) };
        showNotification(GEMINI_NOTIFICATION_ID, message, style, 10000);
    }

    async function handleGemini() {
        debugLog("Gemini页面检测到。检查提示词...");

        const prompt = GM_getValue('geminiPrompt', '');
        const timestamp = GM_getValue('timestamp', 0);
        const videoTitle = GM_getValue('videoTitle', 'N/A');

        // 检查提示词是否存在且未过期
        if (!prompt || Date.now() - timestamp > GEMINI_PROMPT_EXPIRY_MS) {
            debugLog("未找到有效提示词或已过期");
            GM_deleteValue('geminiPrompt');
            GM_deleteValue('timestamp');
            GM_deleteValue('videoTitle');
            return;
        }

        debugLog("找到有效提示词。等待Gemini输入区域...");

        // 使用多个选择器尝试找到输入区
        const textareaSelectors = [
            'div[class*="text-input-field"][class*="with-toolbox-drawer"]',
            'div[class*="input-area"]',
            'div[contenteditable="true"]',
            'div[class*="textarea-wrapper"]',
            'textarea',
            'div[role="textbox"]'
        ];

        try {
            // 等待输入区域出现
            const textarea = await waitForElements(textareaSelectors, GEMINI_ELEMENT_TIMEOUT_MS);
            debugLog("找到输入区域。尝试输入提示词。");

            // 尝试输入文本
            let inputSuccess = false;
            try {
                textarea.focus();

                if (textarea.isContentEditable) {
                    textarea.innerText = prompt;
                } else if (textarea.tagName.toLowerCase() === 'textarea') {
                    textarea.value = prompt;
                } else {
                    document.execCommand('insertText', false, prompt);
                }

                // 触发事件以确保框架检测到变化
                textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
                textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
                inputSuccess = true;
                debugLog("提示词已插入到输入区域。");
            } catch (inputError) {
                debugLog(`插入文本时出错: ${inputError}。尝试剪贴板方法。`);
                showGeminiNotification("无法自动填入提示词。请手动粘贴。\n提示词已复制到剪贴板。", "error");
                copyToClipboard(prompt);
                GM_deleteValue('geminiPrompt');
                GM_deleteValue('timestamp');
                GM_deleteValue('videoTitle');
                return;
            }

            if (inputSuccess) {
                // 短暂延迟,等待UI更新
                await new Promise(resolve => setTimeout(resolve, 100));

                debugLog("等待发送按钮...");
                // 使用多个选择器尝试找到发送按钮
                const sendButtonSelectors = [
                    'button:has(mat-icon[data-mat-icon-name="send"])',
                    'mat-icon[data-mat-icon-name="send"]',
                    'button:has(span.mat-mdc-button-touch-target)',
                    'button.mat-mdc-icon-button',
                    'button[id*="submit"]',
                    'button[aria-label="Run"]',
                    'button[aria-label="Send"]',
                    'button[aria-label="Submit"]',
                    'button[aria-label="发送"]'
                ];

                try {
                    // 等待发送按钮出现
                    let sendButtonElement = await waitForElements(sendButtonSelectors, GEMINI_ELEMENT_TIMEOUT_MS);
                    debugLog("找到发送按钮。");

                    // 如果找到的是图标,获取父按钮
                    if (sendButtonElement.tagName.toLowerCase() === 'mat-icon') {
                        const parentButton = sendButtonElement.closest('button');
                        if (parentButton && !parentButton.disabled) {
                            sendButtonElement = parentButton;
                        } else {
                            throw new Error("找到发送图标,但父按钮缺失或禁用。");
                        }
                    }

                    // 检查按钮是否启用
                    if (sendButtonElement.disabled) {
                        debugLog("发送按钮已禁用。稍等片刻...");
                        await new Promise(resolve => setTimeout(resolve, 500));
                        if (sendButtonElement.disabled) {
                            throw new Error("发送按钮仍然禁用。");
                        }
                    }

                    // 点击按钮
                    sendButtonElement.click();
                    debugLog("成功点击发送按钮。");

                    // 成功通知
                    const successMessage = `
已自动发送视频摘要请求!

正在分析视频: "${videoTitle}"

请稍候,Gemini正在处理您的请求...
                    `;
                    showGeminiNotification(successMessage.trim(), "info");

                    // 清理存储
                    GM_deleteValue('geminiPrompt');
                    GM_deleteValue('timestamp');
                    GM_deleteValue('videoTitle');

                } catch (buttonError) {
                    debugLog(`发送按钮错误: ${buttonError.message}`);
                    showGeminiNotification("找不到或无法点击发送按钮。\n提示词已填入,请手动点击发送。", "warning");
                }
            }

        } catch (textareaError) {
            debugLog(`输入区域错误: ${textareaError.message}`);
            showGeminiNotification("无法找到Gemini输入框。\n请手动粘贴提示词。\n提示词已复制到剪贴板。", "error");
            copyToClipboard(prompt);
            GM_deleteValue('geminiPrompt');
            GM_deleteValue('timestamp');
            GM_deleteValue('videoTitle');
        }
    }

    // --- 主执行逻辑 ---
    // 检测当前网站
    const isYouTube = window.location.hostname.includes('youtube.com');
    const isGemini = window.location.hostname.includes('gemini.google.com');

    if (isYouTube) {
        debugLog("YouTube页面检测到。初始化按钮添加器。");

        // 初始尝试添加按钮
        if (isVideoPage()) {
            addSummarizeButton();
        }

        // 设置URL变化检测
        const originalPushState = history.pushState;
        history.pushState = function() {
            originalPushState.apply(this, arguments);
            debugLog("检测到pushState URL变化");
            setTimeout(() => {
                if (isVideoPage()) {
                    addSummarizeButton();
                }
            }, 500);
        };

        // 监听popstate事件(浏览器前进/后退按钮)
        window.addEventListener('popstate', function() {
            debugLog("检测到popstate URL变化");
            setTimeout(() => {
                if (isVideoPage()) {
                    addSummarizeButton();
                }
            }, 500);
        });

        // 定期检查URL变化
        let lastUrl = location.href;
        setInterval(() => {
            const currentUrl = location.href;
            if (currentUrl !== lastUrl) {
                lastUrl = currentUrl;
                debugLog(`通过轮询检测到URL变化: ${currentUrl}`);
                if (isVideoPage()) {
                    setTimeout(addSummarizeButton, 500);
                }
            }
        }, URL_CHECK_INTERVAL_MS);
    } else if (isGemini) {
        // 等待页面加载后处理Gemini
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => setTimeout(handleGemini, 500));
        } else {
            setTimeout(handleGemini, 500);
        }
    }
})();

QingJ © 2025

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