您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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://gf.zukizuki.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(); })();