YouTube Transcript Copier

Adds a 'Copy Transcript' button to the action bar (Like/Dislike/Share) and copies YouTube video transcripts with timestamps. Auto-expands description.

  1. // ==UserScript==
  2. // @name YouTube Transcript Copier
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3.0
  5. // @description Adds a 'Copy Transcript' button to the action bar (Like/Dislike/Share) and copies YouTube video transcripts with timestamps. Auto-expands description.
  6. // @author MrPickleMna
  7. // @match https://www.youtube.com/watch*
  8. // @grant GM_setClipboard
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. const OUR_BUTTON_ID = 'pragmatic-copy-transcript-button';
  16. const ACTION_BUTTONS_CONTAINER_SELECTOR = '#top-level-buttons-computed';
  17. const LIKE_DISLIKE_SELECTOR = 'segmented-like-dislike-button-view-model';
  18. const SHOW_TRANSCRIPT_SELECTOR = 'ytd-video-description-transcript-section-renderer button[aria-label="Show transcript"]';
  19. const EXPAND_DESC_SELECTOR = '#description-inline-expander #expand';
  20. const TRANSCRIPT_PANEL_SELECTOR = 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-searchable-transcript"]';
  21. const OBSERVER_TARGET_SELECTOR = '#below';
  22.  
  23. console.log('YouTube Transcript Copier: Script initiated (v1.3.0 - Action Bar Button).');
  24.  
  25. function copyTranscript() {
  26. console.log('Copy Transcript button clicked.');
  27.  
  28. const showTranscriptButtonOriginal = document.querySelector(SHOW_TRANSCRIPT_SELECTOR);
  29.  
  30. if (!showTranscriptButtonOriginal) {
  31. console.error("[Copy Transcript] Could not find the *original* 'Show transcript' button in the description area to open the panel.");
  32. alert("Error: Could not find the 'Show transcript' button in the description section. Ensure the description is expanded and the button exists.");
  33. return;
  34. }
  35.  
  36. showTranscriptButtonOriginal.click();
  37. console.log("[Copy Transcript] 'Show transcript' button (original in description) clicked programmatically.");
  38.  
  39. const maxAttempts = 20;
  40. let attempts = 0;
  41. const intervalId = setInterval(() => {
  42. const transcriptPanel = document.querySelector(TRANSCRIPT_PANEL_SELECTOR);
  43. if (transcriptPanel && transcriptPanel.querySelector('ytd-transcript-segment-list-renderer')) {
  44. clearInterval(intervalId);
  45. console.log('[Copy Transcript] Transcript panel found and appears loaded:', transcriptPanel);
  46. let transcriptText = '';
  47. const segments = transcriptPanel.querySelectorAll('ytd-transcript-segment-renderer');
  48. if (segments && segments.length > 0) {
  49. segments.forEach(segment => {
  50. const timestampEl = segment.querySelector('.segment-timestamp');
  51. const textEl = segment.querySelector('yt-formatted-string.segment-text');
  52. if (timestampEl && textEl) {
  53. const timestamp = timestampEl.innerText.trim();
  54. const text = textEl.innerText.trim();
  55. transcriptText += `${timestamp} ${text}\n`;
  56. } else {
  57. transcriptText += `${segment.innerText.trim()}\n`;
  58. }
  59. });
  60. transcriptText = transcriptText.trim();
  61. console.log(`[Copy Transcript] Extracted text from ${segments.length} segments.`);
  62. } else {
  63. console.warn("[Copy Transcript] Could not find transcript segments, falling back to innerText of the panel.");
  64. transcriptText = transcriptPanel.innerText.trim();
  65. }
  66.  
  67. if (transcriptText) {
  68. GM_setClipboard(transcriptText, 'text');
  69. console.log('[Copy Transcript] Transcript copied to clipboard.');
  70. alert('Transcript copied to clipboard!');
  71. } else {
  72. console.error('[Copy Transcript] Transcript panel found, but no text content detected after processing.');
  73. alert('Error: Transcript panel loaded but appears empty or could not extract text.');
  74. }
  75. } else {
  76. attempts++;
  77. if (attempts >= maxAttempts) {
  78. clearInterval(intervalId);
  79. console.error('[Copy Transcript] Timed out waiting for transcript panel to load content.');
  80. alert('Error: Timed out waiting for transcript panel.');
  81. }
  82. }
  83. }, 500);
  84. }
  85.  
  86. function addCopyButtonIfMissing() {
  87. if (document.getElementById(OUR_BUTTON_ID)) {
  88. return;
  89. }
  90.  
  91. const expandButton = document.querySelector(EXPAND_DESC_SELECTOR);
  92. if (expandButton && expandButton.offsetParent !== null) {
  93. console.log('[Add Button] Found "...more" description button. Clicking it.');
  94. expandButton.click();
  95. return;
  96. }
  97.  
  98. const actionButtonsContainer = document.querySelector(ACTION_BUTTONS_CONTAINER_SELECTOR);
  99. const likeDislikeGroup = actionButtonsContainer?.querySelector(LIKE_DISLIKE_SELECTOR);
  100.  
  101. if (actionButtonsContainer && likeDislikeGroup) {
  102. if (document.getElementById(OUR_BUTTON_ID)) {
  103. return;
  104. }
  105. console.log('[Add Button] Found action buttons container and like/dislike group. Preparing to insert button.');
  106.  
  107. const copyButton = document.createElement('button');
  108. copyButton.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';
  109. copyButton.id = OUR_BUTTON_ID;
  110. copyButton.title = 'Copy video transcript';
  111. copyButton.style.marginLeft = '8px';
  112. copyButton.style.marginRight = '8px';
  113.  
  114. const textDiv = document.createElement('div');
  115. textDiv.className = 'yt-spec-button-shape-next__button-text-content';
  116. const textSpan = document.createElement('span');
  117. textSpan.className = 'yt-core-attributed-string yt-core-attributed-string--white-space-no-wrap';
  118. textSpan.setAttribute('role', 'text');
  119. textSpan.innerText = 'Copy Transcript';
  120. textDiv.appendChild(textSpan);
  121. copyButton.appendChild(textDiv);
  122.  
  123. copyButton.addEventListener('click', copyTranscript);
  124.  
  125. likeDislikeGroup.parentNode.insertBefore(copyButton, likeDislikeGroup.nextSibling);
  126. console.log('[Add Button] "Copy Transcript" button inserted into action bar.');
  127.  
  128. } else {
  129.  
  130. }
  131. }
  132.  
  133. console.log('YouTube Transcript Copier: Setting up MutationObserver.');
  134. let observer = null;
  135.  
  136. function startObserver() {
  137. if (observer) {
  138. observer.disconnect();
  139. }
  140.  
  141. const targetNode = document.querySelector(OBSERVER_TARGET_SELECTOR);
  142. if (targetNode) {
  143. observer = new MutationObserver((mutationsList, obs) => {
  144. window.requestAnimationFrame(addCopyButtonIfMissing);
  145. });
  146. observer.observe(targetNode, {
  147. childList: true,
  148. subtree: true
  149. });
  150. window.requestAnimationFrame(addCopyButtonIfMissing);
  151. } else {
  152. console.log(`[Observer] Target node '${OBSERVER_TARGET_SELECTOR}' not found. Retrying in 1 second...`);
  153. setTimeout(startObserver, 1000);
  154. }
  155. }
  156.  
  157. setTimeout(startObserver, 1000);
  158.  
  159. document.addEventListener('yt-navigate-finish', (event) => {
  160. console.log('[Navigation] Detected yt-navigate-finish event. Re-running setup.');
  161. setTimeout(startObserver, 500);
  162. });
  163.  
  164. window.addEventListener('popstate', () => {
  165. console.log('[Navigation] Detected popstate event. Re-running setup.');
  166. setTimeout(startObserver, 500);
  167. });
  168.  
  169. })();