YouTube Transcript search

Adds a search input box on top of the YouTube transcript. Press enter on it to only show lines containing the inputted search term.

  1. // ==UserScript==
  2. // @name YouTube Transcript search
  3. // @namespace https://greasyfork.org/en/users/1436613-gosha305
  4. // @version 2.1.1
  5. // @license MIT
  6. // @description Adds a search input box on top of the YouTube transcript. Press enter on it to only show lines containing the inputted search term.
  7. // @author gosha305
  8. // @match https://www.youtube.com/*
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. "use strict";
  13. const removeInnateTranscriptSearch = document.createElement("style");
  14. removeInnateTranscriptSearch.textContent = "ytd-transcript-search-box-renderer { display: none; }";
  15. document.head.appendChild(removeInnateTranscriptSearch);
  16.  
  17. const inputContainer = document.createElement("div");
  18. inputContainer.style.padding = "10px";
  19. inputContainer.style.display = "block";
  20. inputContainer.classList.add("ytSearchboxComponentInput");
  21.  
  22. const inputBox = document.createElement("input");
  23. inputBox.type = "text";
  24. inputBox.placeholder = "Search...";
  25. inputBox.style.color = "white";
  26. inputBox.style.backgroundColor = "black";
  27. inputBox.style.width = "90%";
  28.  
  29. inputContainer.appendChild(inputBox);
  30. const secondaryInputContainer = inputContainer.cloneNode(true);
  31. const secondaryInputBox = secondaryInputContainer.firstChild;
  32. let segments;
  33. let headers;
  34.  
  35. function search(filterWord) {
  36. segments.forEach((segment) => {
  37. if (!segment.textContent.toLowerCase().includes(filterWord)) {
  38. segment.style.display = "none";
  39. } else {
  40. segment.style.display = "flex";
  41. }
  42. });
  43. if (headers) {
  44. headers.forEach((header) => {
  45. if (!header.textContent.toLowerCase().includes(filterWord)) {
  46. header.style.display = "none";
  47. } else {
  48. header.style.display = "flex";
  49. }
  50. });
  51. }
  52. }
  53.  
  54. function clearSearch() {
  55. segments.forEach((segment) => {
  56. segment.style.display = "flex";
  57. });
  58. if (headers) {
  59. headers.forEach((header) => {
  60. header.style.display = "flex";
  61. });
  62. }
  63. }
  64.  
  65. const inputObserver = new MutationObserver(function () {
  66. if (inputContainer.getBoundingClientRect().width == 0 && secondaryInputContainer.getBoundingClientRect().width == 0) {
  67. return;
  68. }
  69. if (inputContainer.getBoundingClientRect().width != 0) {
  70. inputBox.value = "";
  71. inputBox.focus();
  72. }
  73. else {
  74. secondaryInputBox.value = "";
  75. secondaryInputBox.focus();
  76. }
  77. if (segments) { clearSearch() }
  78. });
  79.  
  80. function detectTranscriptPanel() {
  81. if (
  82. document.querySelector(
  83. "ytd-engagement-panel-section-list-renderer:not([target-id])"
  84. ) && document.querySelector(
  85. "ytd-engagement-panel-section-list-renderer[target-id='engagement-panel-searchable-transcript']"
  86. )
  87. ) {
  88. const transcriptPanel = document.querySelector(
  89. "ytd-engagement-panel-section-list-renderer[target-id='engagement-panel-searchable-transcript']"
  90. )
  91. const secondaryTranscriptPanel = document.querySelector(
  92. "ytd-engagement-panel-section-list-renderer:not([target-id])"
  93. );
  94. secondaryTranscriptPanel.insertBefore(secondaryInputContainer, secondaryTranscriptPanel.querySelector("#content"));
  95. transcriptPanel.insertBefore(inputContainer, transcriptPanel.querySelector("#content"));
  96. inputObserver.observe(transcriptPanel, { attributes: true });
  97. inputObserver.observe(secondaryTranscriptPanel, { attributes: true });
  98. }
  99. }
  100.  
  101. const documentObserver = new MutationObserver(() => {
  102. if (document.getElementById("panels")) {
  103. documentObserver.disconnect();
  104. const panelObserver = new MutationObserver(detectTranscriptPanel);
  105. panelObserver.observe(document.getElementById("panels"), { childList: true });
  106. }
  107. });
  108. documentObserver.observe(document.body, { childList: true, subtree: true });
  109.  
  110. document.addEventListener("keydown", function (event) {
  111. if (document.activeElement === inputBox || document.activeElement === secondaryInputBox) {
  112. if (event.key === "Enter") {
  113. if (
  114. !segments
  115. || segments[0] != inputContainer?.parentElement?.querySelector(
  116. "div.segment.style-scope.ytd-transcript-segment-renderer"
  117. )
  118. && segments[0] != secondaryInputContainer?.parentElement?.querySelector(
  119. "div.segment.style-scope.ytd-transcript-segment-renderer"
  120. )
  121. ) {
  122. segments = document.querySelectorAll(
  123. "div.segment.style-scope.ytd-transcript-segment-renderer"
  124. );
  125. headers = document.querySelectorAll(
  126. "div.ytd-transcript-section-header-renderer"
  127. );
  128. }
  129. const filterWord = inputBox.value.trim().toLowerCase() || secondaryInputBox.value.trim().toLowerCase();
  130. if (!filterWord) {
  131. clearSearch();
  132. } else {
  133. search(filterWord);
  134. }
  135. }
  136. }
  137. });
  138.  
  139. document.addEventListener("keydown", function (event) {
  140. if (event.key == "Escape") {
  141. if (document.activeElement === inputBox) inputBox.blur();
  142. if (document.activeElement === secondaryInputBox) secondaryInputBox.blur();
  143. }
  144. });
  145. })();