Greasy Fork 支持简体中文。

YouTube Transcript Copier

Adds a styled button to copy the YouTube video transcript, with a timestamp toggle

// ==UserScript==
// @name         YouTube Transcript Copier
// @match        https://www.youtube.com/watch*
// @license      MIT
// @grant        none
// @version      1.0
// @author       Amir Tehrani
// @description  Adds a styled button to copy the YouTube video transcript, with a timestamp toggle
// @namespace    https://greasyfork.org/
// @icon         https://www.google.com/s2/favicons?domain=youtube.com
// ==/UserScript==

(function() {
    'use strict';

    let observer = null;
    let currentURL = window.location.href;
    let insertionAttempts = 0;
    const maxAttempts = 20;
    let retryInterval = null;
    let includeTimestamps = false; // Default: no timestamps
    let copyButton = null;
    let buttonTextNode = null;
    let transcriptPanelTimeout = null; // Timeout for panel loading
    let transcriptButtonTimeout = null; // Timeout for the "Show transcript" button

    function createTranscriptButton() {
        if (document.getElementById('show-transcript-button')) {
            return true;
        }

        copyButton = document.createElement('button');
        copyButton.id = 'show-transcript-button';
        copyButton.classList.add('yt-transcript-button');
        copyButton.setAttribute('aria-label', 'Copy Transcript'); // Accessibility

        buttonTextNode = document.createTextNode('Copy Transcript');
        copyButton.appendChild(buttonTextNode);

        const timestampSpan = document.createElement('span');
        timestampSpan.id = 'timestamp-toggle';
        timestampSpan.textContent = ' (No Time)';
        timestampSpan.style.cssText = `
            font-size: 0.75em;
            margin-left: 6px;
            color: rgba(255, 255, 255, 0.7);
            cursor: pointer;
            user-select: none;
            border: 1px solid rgba(255, 255, 255, 0.3);
            border-radius: 12px;
            padding: 3px 6px;
            display: inline-block;
            vertical-align: middle;
            transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
            background-color: rgba(0, 0, 0, 0.1);
        `;

        timestampSpan.addEventListener('mouseover', function() {
            this.style.borderColor = 'rgba(255, 255, 255, 0.9)';
            this.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
        });
        timestampSpan.addEventListener('mouseout', function() {
            this.style.borderColor = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.3)';
            this.style.backgroundColor = includeTimestamps ? 'rgba(0,0,0, 0.4)' : 'rgba(0, 0, 0, 0.1)';
        });

        copyButton.appendChild(timestampSpan);

        timestampSpan.addEventListener('click', function(event) {
            event.stopPropagation(); // Prevent main button click
            includeTimestamps = !includeTimestamps;
            this.textContent = includeTimestamps ? ' (Time)' : ' (No Time)';
            this.style.color = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.7)';
            this.style.borderColor = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.3)';
            this.style.backgroundColor = includeTimestamps ? 'rgba(0,0,0, 0.4)' : 'rgba(0, 0, 0, 0.1)';
        });

        copyButton.addEventListener('click', handleCopyClick);

        function insertButton() {
            const potentialTargets = [
                '#description #top-level-buttons-computed',
                '#top-row.ytd-watch-metadata',
                '#above-the-fold',
                '#meta-contents',
                '#primary-inner'
            ];

            for (const targetSelector of potentialTargets) {
                const targetElement = document.querySelector(targetSelector);
                if (targetElement) {
                    targetElement.parentNode.insertBefore(copyButton, targetElement.nextSibling);
                    injectStyles();
                    return true;
                }
            }
            return false;
        }

        return insertButton();
    }

    function handleCopyClick() {
        updateButtonText('Copy Transcript');

        const moreActionsButton = document.querySelector('button[aria-label="More actions"]');
        if (moreActionsButton) {
            moreActionsButton.click();
        }

        const buttonIntervalId = setInterval(() => {
            const transcriptButton = document.querySelector('[aria-label="Show transcript"]');
            if (transcriptButton) {
                transcriptButton.click();
                clearInterval(buttonIntervalId);
                clearTimeout(transcriptButtonTimeout);

                const panelIntervalId = setInterval(() => {
                    const transcriptPanel = document.querySelector('ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-searchable-transcript"] #content');
                    if (transcriptPanel && transcriptPanel.querySelector('ytd-transcript-segment-renderer')) {
                        clearInterval(panelIntervalId);
                        clearTimeout(transcriptPanelTimeout);
                        copyTranscriptText(transcriptPanel);
                    }
                }, 100);

                transcriptPanelTimeout = setTimeout(() => {
                    clearInterval(panelIntervalId);
                    console.error("Transcript panel or segments not found after timeout.");
                    if (copyButton) {
                        updateButtonText("Transcript Not Found");
                        copyButton.style.backgroundColor = "rgba(220, 53, 69, 0.8)";
                    }
                }, 15000);
            }
        }, 250);

        transcriptButtonTimeout = setTimeout(() => {
            clearInterval(buttonIntervalId);
            if (copyButton) {
                updateButtonText("Transcript Not Found")
                copyButton.style.backgroundColor = "rgba(220, 53, 69, 0.8)";
            }
            console.error("Transcript button not found after timeout.");
        }, 10000);
    }

    function copyTranscriptText(transcriptPanel) {
        if (!transcriptPanel) {
            console.error("Transcript container not found.");
            updateButtonText("Error");
            return;
        }

        let transcriptText = "";

        if (includeTimestamps) {
            transcriptPanel.querySelectorAll('ytd-transcript-segment-renderer').forEach(line => {
                const timestampElement = line.querySelector('.segment-timestamp');
                const textElement = line.querySelector('.segment-text');
                if (timestampElement && textElement) {
                    transcriptText += timestampElement.textContent.trim() + " " + textElement.textContent.trim() + "\n";
                }
            });
        } else {
            transcriptPanel.querySelectorAll('.segment-text').forEach(segment => {
                transcriptText += segment.textContent.trim() + " ";
            });
        }

        navigator.clipboard.writeText(transcriptText)
            .then(() => {
              updateButtonText("Copied!");
            })
            .catch(err => {
                console.error('Failed to copy transcript:', err);
                if (copyButton) {
                    updateButtonText("Copy Failed");
                    copyButton.style.backgroundColor = "rgba(220, 53, 69, 0.8)";
                }
            });
    }

    function updateButtonText(text) {
        if (copyButton && buttonTextNode) {
            buttonTextNode.textContent = text;
            if (text === "Copied!") {
                copyButton.style.backgroundColor = "rgba(40, 167, 69, 0.9)";
                  setTimeout(() => {
                        buttonTextNode.textContent = 'Copy Transcript';
                         copyButton.style.backgroundColor = 'rgba(0, 123, 255, 0.8)';

                         const timestampToggle = document.getElementById('timestamp-toggle');
                         timestampToggle.textContent = includeTimestamps ? ' (Time)' : ' (No Time)';
                         timestampToggle.style.color = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.7)';
                         timestampToggle.style.borderColor = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.3)';
                         timestampToggle.style.backgroundColor = includeTimestamps ? 'rgba(0,0,0, 0.4)' : 'rgba(0, 0, 0, 0.1)';

                    }, 1500);
            } else if (text.startsWith("Error") || text === "Copy Failed" || text === "Transcript Not Found") {
                 copyButton.style.backgroundColor = "rgba(220, 53, 69, 0.8)";
            }
        }
    }

    function injectStyles() {
        if (document.getElementById('yt-transcript-button-styles')) return;

        const style = document.createElement('style');
        style.id = 'yt-transcript-button-styles';
        style.textContent = `
            .yt-transcript-button {
                background-color: rgba(0, 123, 255, 0.8);
                border: none;
                color: white;
                padding: 10px 18px;
                text-align: center;
                text-decoration: none;
                display: inline-flex;
                align-items: center;
                font-size: 15px;
                margin: 4px 2px;
                cursor: pointer;
                border-radius: 24px;
                transition: all 0.2s ease;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
                font-family: 'Roboto', sans-serif;
                font-weight: 500;
                position: relative;
                overflow: hidden;
                will-change: transform, box-shadow, background-color;
            }

            .yt-transcript-button:hover {
                background-color: rgba(0, 90, 180, 0.9);
                box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
                transform: translateY(-1px);
            }

            .yt-transcript-button:focus {
                outline: none;
                box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.3);
            }

            .yt-transcript-button:active {
                background-color: rgba(0, 60, 120, 0.9);
                box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
                transform: translateY(1px);
            }

            .yt-transcript-button::before {
                content: '';
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: radial-gradient(circle at 0 0, rgba(255, 255, 255, 0.4) 10%, transparent 10.01%);
                background-size: 0 0;
                opacity: 0;
                pointer-events: none;
                transition: background-size 0.4s ease, opacity 0.4s ease, background-position 0.4s ease;
            }

            .yt-transcript-button:active::before {
                background-size: 200% 200%;
                opacity: 1;
                transition: background-size 0.4s ease, opacity 0.4s ease;
            }
        `;
        document.head.appendChild(style);

        copyButton.addEventListener('mousedown', function(event) {
            const rect = copyButton.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;

            copyButton.style.setProperty('--ripple-x', x + 'px');
            copyButton.style.setProperty('--ripple-y', y + 'px');
            copyButton.style.background = `radial-gradient(circle at var(--ripple-x) var(--ripple-y), rgba(255, 255, 255, 0.4) 10%, transparent 10.01%)`;
        });

        copyButton.addEventListener('mouseup', function(event){
            copyButton.style.background = null;
        });

        copyButton.addEventListener("mouseleave", function(event) {
            copyButton.style.background = null;
        });
    }

    function attemptButtonCreation() {
        insertionAttempts++;
        if (createTranscriptButton() || insertionAttempts >= maxAttempts) {
            clearInterval(retryInterval);
            retryInterval = null;
            if (insertionAttempts >= maxAttempts) {
                console.error('Could not insert the button after multiple attempts.');
            }
        }
    }

    function setupObserver() {
        if (observer) observer.disconnect();
        observer = new MutationObserver(handleMutations);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function handleMutations(mutations) {
        if (window.location.href !== currentURL) {
            currentURL = window.location.href;
            resetState();
            startProcess();
        }
    }

    function resetState() {
        if (document.getElementById('show-transcript-button')) {
            document.getElementById('show-transcript-button').remove();
        }
        insertionAttempts = 0;
        if (retryInterval) {
            clearInterval(retryInterval);
            retryInterval = null;
        }
        clearTimeout(transcriptPanelTimeout);
        clearTimeout(transcriptButtonTimeout);
        transcriptPanelTimeout = null;
        transcriptButtonTimeout = null;

        if (observer) {
          observer.disconnect();
          observer = null;
        }

        copyButton = null;
        buttonTextNode = null;
    }

    function startProcess() {
        if (!createTranscriptButton()) {
            retryInterval = setInterval(attemptButtonCreation, 500);
        }
        setupObserver();
    }

    startProcess();
})();