🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

YouTube Transcript Copier

Adds a button to copy the video transcript next to the like/share buttons. Safely handles Trusted Types and SPA navigation.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         YouTube Transcript Copier
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds a button to copy the video transcript next to the like/share buttons. Safely handles Trusted Types and SPA navigation.
// @author       You
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const SELECTORS = {
        BAR: '#top-level-buttons-computed',
        TRANSCRIPT_RENDERER: 'ytd-transcript-renderer',
        SEGMENT: 'ytd-transcript-segment-renderer',
        TIMESTAMP: '.segment-timestamp',
        TEXT: '.segment-text',
        SHOW_TRANSCRIPT_BTN: 'button[aria-label="Show transcript"]',
        EXPAND_DESC_BTN: '#expand',
        DESCRIPTION_CONTAINER: 'ytd-text-inline-expander'
    };

    const BUTTON_ID = 'yt-custom-transcript-copy-btn';

    // SVG Icon Data for the Clipboard (Safe creation)
    function createCopyIcon() {
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("viewBox", "0 0 24 24");
        svg.setAttribute("height", "24");
        svg.setAttribute("width", "24");
        svg.setAttribute("focusable", "false");
        svg.style.display = "block";
        svg.style.fill = "currentColor";

        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("d", "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z");

        svg.appendChild(path);
        return svg;
    }

    // Helper to wait for elements (needed because transcript loads lazily)
    function waitForElement(selector, timeout = 2000) {
        return new Promise((resolve) => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }
            const observer = new MutationObserver((mutations) => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => {
                observer.disconnect();
                resolve(null);
            }, timeout);
        });
    }

    // The Main Copy Logic
    async function handleCopyClick(btnElement) {
        const originalText = btnElement.innerText;
        btnElement.innerText = "Loading...";

        try {
            // 1. Check if transcript is visible
            let segments = document.querySelectorAll(SELECTORS.SEGMENT);

            // 2. If not, try to open it automatically
            if (segments.length === 0) {
                // Check for "Show Transcript" button immediately
                let showBtn = Array.from(document.querySelectorAll('button')).find(b =>
                    b.ariaLabel === 'Show transcript' || (b.textContent && b.textContent.includes('Show transcript'))
                );

                // If not found, try expanding the description
                if (!showBtn) {
                    const expandBtn = document.querySelector(SELECTORS.EXPAND_DESC_BTN);
                    if (expandBtn && expandBtn.offsetParent !== null) { // Check visibility
                        expandBtn.click();
                        // Short wait for description expansion
                        await new Promise(r => setTimeout(r, 500));

                        // Look again for show transcript button
                        showBtn = Array.from(document.querySelectorAll('button')).find(b =>
                             b.ariaLabel === 'Show transcript' || (b.textContent && b.textContent.includes('Show transcript'))
                        );
                    }
                }

                if (showBtn) {
                    showBtn.click();
                    // Wait for the transcript panel to render
                    await waitForElement(SELECTORS.SEGMENT, 3000);
                    segments = document.querySelectorAll(SELECTORS.SEGMENT);
                }
            }

            // 3. Scrape Data
            if (segments.length > 0) {
                let transcriptText = "";
                segments.forEach(seg => {
                    const time = seg.querySelector(SELECTORS.TIMESTAMP)?.textContent?.trim().replace(/\s+/g, ' ') || "";
                    const text = seg.querySelector(SELECTORS.TEXT)?.textContent?.trim().replace(/\s+/g, ' ') || "";
                    if (text) {
                        transcriptText += `[${time}] ${text}\n`;
                    }
                });

                if (transcriptText) {
                    await navigator.clipboard.writeText(transcriptText);
                    btnElement.innerText = "Copied!";
                } else {
                    btnElement.innerText = "Empty!";
                }
            } else {
                alert("Could not find transcript. Please open the transcript panel manually.");
                btnElement.innerText = "Failed";
            }

        } catch (err) {
            console.error("Transcript copy error:", err);
            btnElement.innerText = "Error";
        }

        // Reset button text after 2 seconds
        setTimeout(() => {
            // Re-build the inner structure of the button safely
            btnElement.innerText = "";
            const iconDiv = document.createElement("div");
            iconDiv.className = "yt-spec-button-shape-next__icon";
            iconDiv.setAttribute("aria-hidden", "true");

            const iconWrapper = document.createElement("span");
            iconWrapper.style.width = "24px";
            iconWrapper.style.height = "24px";
            iconWrapper.style.display = "inline-block";

            iconWrapper.appendChild(createCopyIcon());
            iconDiv.appendChild(iconWrapper);

            const textDiv = document.createElement("div");
            textDiv.className = "yt-spec-button-shape-next__button-text-content";
            textDiv.innerText = "Transcript";
            textDiv.style.marginLeft = "6px";

            btnElement.appendChild(iconDiv);
            btnElement.appendChild(textDiv);
        }, 2000);
    }

    // Function to inject the button
    function injectButton() {
        // Prevent duplicate injection
        if (document.getElementById(BUTTON_ID)) return;

        const container = document.querySelector(SELECTORS.BAR);
        if (!container) return;

        // Create the Button Wrapper (ViewModel mimic)
        const wrapper = document.createElement('div');
        wrapper.className = 'yt-button-view-model style-scope ytd-menu-renderer';
        wrapper.id = BUTTON_ID;
        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'center';
        wrapper.style.marginLeft = '8px'; // Spacing from other buttons

        // Create the Button (mimicking Share button classes)
        const button = document.createElement('button');
        button.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading yt-spec-button-shape-next--enable-backdrop-filter-experiment';
        button.setAttribute('aria-label', 'Copy Transcript');
        button.style.cursor = 'pointer';

        // 1. Icon Container
        const iconDiv = document.createElement("div");
        iconDiv.className = "yt-spec-button-shape-next__icon";
        iconDiv.setAttribute("aria-hidden", "true");

        // 2. Icon Content
        const iconWrapper = document.createElement("span");
        iconWrapper.style.width = "24px";
        iconWrapper.style.height = "24px";
        iconWrapper.style.display = "inline-block";
        iconWrapper.appendChild(createCopyIcon()); // Safe DOM node append
        iconDiv.appendChild(iconWrapper);

        // 3. Text Content
        const textDiv = document.createElement("div");
        textDiv.className = "yt-spec-button-shape-next__button-text-content";
        textDiv.innerText = "Transcript";

        // Append parts to button
        button.appendChild(iconDiv);
        button.appendChild(textDiv);

        // Add Click Listener
        button.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            handleCopyClick(button);
        });

        wrapper.appendChild(button);

        // Insert as the first item in the menu container, or append
        // Appending usually puts it next to "Share" or "Download"
        container.appendChild(wrapper);
    }

    // Observer to handle SPA navigation and dynamic loading
    const observer = new MutationObserver((mutations) => {
        // Check if our target container exists but our button doesn't
        const container = document.querySelector(SELECTORS.BAR);
        if (container && !document.getElementById(BUTTON_ID)) {
            injectButton();
        }
    });

    // Start Observing
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Initial check
    injectButton();

})();