YouTube Transcript search

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

目前為 2025-03-02 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name YouTube Transcript search
  3. // @namespace https://greasyfork.org/en/users/1436613-gosha305
  4. // @version 2.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 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. 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. console.log(secondaryTranscriptPanel);
  95. secondaryTranscriptPanel.insertBefore(secondaryInputContainer, secondaryTranscriptPanel.querySelector("#content"));
  96. transcriptPanel.querySelector("#header").appendChild(inputContainer);
  97. inputObserver.observe(transcriptPanel, { attributes: true });
  98. inputObserver.observe(secondaryTranscriptPanel, { attributes: true });
  99. }
  100. }
  101.  
  102. const documentObserver = new MutationObserver(() => {
  103. if (document.getElementById("panels")) {
  104. documentObserver.disconnect();
  105. const mutationObserverTarget = document.getElementById("panels");
  106. const mutationObserverConfig = { childList: true };
  107. const panelObserver = new MutationObserver(detectTranscriptPanel);
  108. panelObserver.observe(mutationObserverTarget, mutationObserverConfig);
  109. }
  110. });
  111. documentObserver.observe(document.body, { childList: true, subtree: true });
  112.  
  113. document.addEventListener("keydown", function (event) {
  114. if (document.activeElement === inputBox || document.activeElement === secondaryInputBox) {
  115. if (event.key === "Enter") {
  116. if (
  117. !segments ||
  118. segments[0] !=
  119. inputContainer.parentElement.querySelector(
  120. "div.segment.style-scope.ytd-transcript-segment-renderer"
  121. )
  122. ) {
  123. segments = document.querySelectorAll(
  124. "div.segment.style-scope.ytd-transcript-segment-renderer"
  125. );
  126. headers = document.querySelectorAll(
  127. "div.ytd-transcript-section-header-renderer"
  128. );
  129. }
  130. const filterWord = inputBox.value.trim().toLowerCase() || secondaryInputBox.value.trim().toLowerCase();
  131. if (!filterWord) {
  132. clearSearch();
  133. } else {
  134. search(filterWord);
  135. }
  136. }
  137. }
  138. });
  139.  
  140. document.addEventListener("keydown", function (event) {
  141. if (event.key == "Escape") {
  142. if (document.activeElement === inputBox) inputBox.blur();
  143. if (document.activeElement === secondaryInputBox) secondaryInputBox.blur();
  144. }
  145. });
  146. })();