Greasy Fork is available in English.

Youtube - Search While Watching Video

Search YouTube without interrupting the video, by loading the search results in the related video bar

  1. // ==UserScript==
  2. // @name Youtube - Search While Watching Video
  3. // @version 2.5.5
  4. // @description Search YouTube without interrupting the video, by loading the search results in the related video bar
  5. // @author Cpt_mathix
  6. // @match https://www.youtube.com/*
  7. // @license GPL-2.0-or-later
  8. // @require https://cdn.jsdelivr.net/gh/culefa/JavaScript-autoComplete@19203f30f148e2d9d810ece292b987abb157bbe0/auto-complete.min.js
  9. // @namespace https://greasyfork.org/users/16080
  10. // @run-at document-start
  11. // @grant none
  12. // @noframes
  13. // ==/UserScript==
  14.  
  15. /* jshint esversion: 11 */
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // Handle Trusted Type Policy Violations
  21. var TTP = window.TTP = {createHTML: (string, sink) => string, createScript: (string, sink) => string, createScriptURL: (string, sink) => string};
  22. if(typeof window.isSecureContext !== 'undefined' && window.isSecureContext){
  23. if (window.trustedTypes && window.trustedTypes.createPolicy){
  24. if(window.trustedTypes.defaultPolicy) {
  25. TTP = window.TTP = window.trustedTypes.defaultPolicy;
  26. } else {
  27. TTP = window.TTP = window.trustedTypes.createPolicy("default", TTP);
  28. }
  29. }
  30. }
  31.  
  32. function youtube_search_while_watching_video() {
  33. let script = {
  34. initialized: false,
  35.  
  36. ytplayer: null,
  37.  
  38. search_bar: null,
  39. search_autocomplete: null,
  40. search_suggestions: [],
  41. searched: false,
  42.  
  43. debug: false
  44. };
  45.  
  46. const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);
  47.  
  48. document.addEventListener("DOMContentLoaded", initScript);
  49.  
  50. // reload script on page change using youtube polymer fire events
  51. window.addEventListener("yt-page-data-updated", function(event) {
  52. if (script.debug) { console.log("# page updated #"); }
  53. cleanupSearch();
  54. startScript(2);
  55. });
  56.  
  57. function initScript() {
  58. if (script.debug) { console.log("### Youtube Search While Watching Video Initializing ###"); }
  59.  
  60. initSearch();
  61. injectCSS();
  62.  
  63. if (script.debug) { console.log("### Youtube Search While Watching Video Initialized ###"); }
  64. script.initialized = true;
  65.  
  66. startScript(5);
  67. }
  68.  
  69. function startScript(retry) {
  70. if (script.initialized && isPlayerAvailable()) {
  71. if (script.debug) { console.log("videoplayer is available"); }
  72. if (script.debug) { console.log("ytplayer: ", script.ytplayer); }
  73.  
  74. if (script.ytplayer) {
  75. try {
  76. if (script.debug) { console.log("initializing search"); }
  77. loadSearch();
  78. } catch (error) {
  79. console.log("Failed to initialize search: ", (script.debug) ? error : error.message);
  80. }
  81. }
  82. } else if (retry > 0) { // fix conflict with Youtube+ script
  83. setTimeout( function() {
  84. startScript(--retry);
  85. }, 1000);
  86. } else {
  87. if (script.debug) { console.log("videoplayer is unavailable"); }
  88. }
  89. }
  90.  
  91. // *** VIDEOPLAYER *** //
  92.  
  93. function getVideoPlayer() {
  94. return insp(document.getElementById('movie_player'));
  95. }
  96.  
  97. function isPlayerAvailable() {
  98. script.ytplayer = getVideoPlayer();
  99. return script.ytplayer !== null && script.ytplayer.getVideoData?.().video_id;
  100. }
  101.  
  102. // *** SEARCH *** //
  103.  
  104. function initSearch() {
  105. // callback function for search suggestion results
  106. window.suggestions_callback = suggestionsCallback;
  107. }
  108.  
  109. function loadSearch() {
  110. // prevent double searchbar
  111. let playlistOrLiveSearchBar = document.querySelector('#suggestions-search.playlist-or-live');
  112. if (playlistOrLiveSearchBar) { playlistOrLiveSearchBar.remove(); }
  113.  
  114. let searchbar = document.getElementById('suggestions-search');
  115. if (!searchbar) {
  116. createSearchBar();
  117. } else {
  118. searchbar.value = "";
  119. }
  120.  
  121. script.searched = false;
  122. cleanupSuggestionRequests();
  123. }
  124.  
  125. function cleanupSearch() {
  126. if (script.search_autocomplete) {
  127. script.search_autocomplete.destroy();
  128. }
  129. }
  130.  
  131. function createSearchBar() {
  132. let anchor, html;
  133.  
  134. anchor = document.querySelector('ytd-compact-autoplay-renderer > #contents');
  135. if (anchor) {
  136. html = "<input id=\"suggestions-search\" type=\"search\" placeholder=\"Search\">";
  137. anchor.insertAdjacentHTML("afterend", html);
  138. } else { // playlist, live video or experimental youtube layout (where autoplay is not a separate renderer anymore)
  139. anchor = document.querySelector('#related > ytd-watch-next-secondary-results-renderer');
  140. if (anchor) {
  141. html = "<input id=\"suggestions-search\" class=\"playlist-or-live\" type=\"search\" placeholder=\"Search\">";
  142. anchor.insertAdjacentHTML("beforebegin", html);
  143. }
  144. }
  145.  
  146. let searchBar = document.getElementById('suggestions-search');
  147. if (searchBar) {
  148. script.search_bar = searchBar;
  149.  
  150. script.search_autocomplete = new window.autoComplete({
  151. selector: '#suggestions-search',
  152. minChars: 1,
  153. delay: 100,
  154. source: function(term, suggest) {
  155. script.search_suggestions = {
  156. query: term,
  157. suggest: suggest
  158. };
  159. searchSuggestions(term);
  160. },
  161. onSelect: function(event, term, item) {
  162. prepareNewSearchRequest(term);
  163. }
  164. });
  165.  
  166. script.search_bar.addEventListener("keyup", function(event) {
  167. if (this.value === "") {
  168. resetSuggestions();
  169. }
  170. });
  171.  
  172. // seperate keydown listener because the search listener blocks keyup..?
  173. script.search_bar.addEventListener("keydown", function(event) {
  174. const ENTER = 13;
  175. if (this.value.trim() !== "" && (event.key == "Enter" || event.keyCode === ENTER)) {
  176. prepareNewSearchRequest(this.value.trim());
  177. }
  178. });
  179.  
  180. script.search_bar.addEventListener("search", function(event) {
  181. if(this.value === "") {
  182. script.search_bar.blur(); // close search suggestions dropdown
  183. script.search_suggestions = []; // clearing the search suggestions
  184.  
  185. resetSuggestions();
  186. }
  187. });
  188.  
  189. script.search_bar.addEventListener("focus", function(event) {
  190. this.select();
  191. });
  192. }
  193. }
  194.  
  195. // callback from search suggestions attached to window
  196. function suggestionsCallback(data) {
  197. if (script.debug) { console.log(data); }
  198.  
  199. let query = data[0];
  200. if (query !== script.search_suggestions.query) {
  201. return;
  202. }
  203.  
  204. let raw = data[1]; // extract relevant data from json
  205. let suggestions = raw.map(function(array) {
  206. return array[0]; // change 2D array to 1D array with only suggestions
  207. });
  208.  
  209. script.search_suggestions.suggest(suggestions);
  210. }
  211.  
  212. function searchSuggestions(query) {
  213. // youtube search parameters
  214. const GeoLocation = window.yt.config_.INNERTUBE_CONTEXT_GL;
  215. const HostLanguage = window.yt.config_.INNERTUBE_CONTEXT_HL;
  216.  
  217. if (script.debug) { console.log("suggestion request send", query); }
  218. let scriptElement = document.createElement("script");
  219. scriptElement.type = "text/javascript";
  220. scriptElement.className = "suggestion-request";
  221. scriptElement.src = "https://clients1.google.com/complete/search?client=youtube&hl=" + HostLanguage + "&gl=" + GeoLocation + "&gs_ri=youtube&ds=yt&q=" + encodeURIComponent(query) + "&callback=suggestions_callback";
  222. (document.body || document.head || document.documentElement).appendChild(scriptElement);
  223. }
  224.  
  225. function cleanupSuggestionRequests() {
  226. let requests = document.getElementsByClassName('suggestion-request');
  227. forEachReverse(requests, function(request) {
  228. request.remove();
  229. });
  230. }
  231.  
  232. // send new search request (with the search bar)
  233. function prepareNewSearchRequest(value) {
  234. if (script.debug) { console.log("searching for " + value); }
  235.  
  236. script.search_bar.blur(); // close search suggestions dropdown
  237. script.search_suggestions = []; // clearing the search suggestions
  238. cleanupSuggestionRequests();
  239.  
  240. sendSearchRequest("https://www.youtube.com/results?pbj=1&search_query=" + encodeURIComponent(value));
  241. }
  242.  
  243. // given the url, retrieve the search results
  244. function sendSearchRequest(url) {
  245. let xmlHttp = new XMLHttpRequest();
  246. xmlHttp.onreadystatechange = function() {
  247. if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
  248. processSearch(xmlHttp.responseText);
  249. }
  250. };
  251.  
  252. xmlHttp.open("GET", url, true);
  253. xmlHttp.setRequestHeader("x-youtube-client-name", window.yt.config_.INNERTUBE_CONTEXT_CLIENT_NAME);
  254. xmlHttp.setRequestHeader("x-youtube-client-version", window.yt.config_.INNERTUBE_CONTEXT_CLIENT_VERSION);
  255. xmlHttp.setRequestHeader("x-youtube-client-utc-offset", new Date().getTimezoneOffset() * -1);
  256.  
  257. if (window.yt.config_.ID_TOKEN) { // null if not logged in
  258. xmlHttp.setRequestHeader("x-youtube-identity-token", window.yt.config_.ID_TOKEN);
  259. }
  260.  
  261. xmlHttp.send(null);
  262. }
  263.  
  264. // process search request
  265. function processSearch(responseText) {
  266. try {
  267. let data = JSON.parse(responseText);
  268.  
  269. let found = searchJson(data, (key, value) => {
  270. if (key === "itemSectionRenderer") {
  271. if (script.debug) { console.log(value.contents); }
  272. let succeeded = createSuggestions(value.contents);
  273. return succeeded;
  274. }
  275. return false;
  276. });
  277.  
  278. if (!found) {
  279. alert("The search request was succesful but the script was unable to parse the results");
  280. }
  281. } catch (error) {
  282. alert("Failed to retrieve search data, sorry!\nError message: " + error.message + "\nSearch response: " + responseText);
  283. }
  284. }
  285.  
  286. function searchJson(json, func) {
  287. let found = false;
  288.  
  289. for (let item in json) {
  290. found = func(item, json[item]);
  291. if (found) { break; }
  292.  
  293. if (json[item] !== null && typeof(json[item]) == "object") {
  294. found = searchJson(json[item], func);
  295. if (found) { break; }
  296. }
  297. }
  298.  
  299. return found;
  300. }
  301.  
  302. // *** HTML & CSS *** //
  303.  
  304. function createSuggestions(data) {
  305. // filter out promotional stuff
  306. if (data.length < 10) {
  307. return false;
  308. }
  309.  
  310. // remove current suggestions
  311. let hidden_continuation_item_renderer;
  312. let watchRelated = document.querySelector('#related ytd-watch-next-secondary-results-renderer #items ytd-item-section-renderer #contents') || document.querySelector('#related ytd-watch-next-secondary-results-renderer #items');
  313. forEachReverse(watchRelated.children, function(item) {
  314. if (item.tagName === "YTD-CONTINUATION-ITEM-RENDERER") {
  315. item.setAttribute("hidden", "");
  316. hidden_continuation_item_renderer = item;
  317. } else if (item.tagName !== "YTD-COMPACT-AUTOPLAY-RENDERER") {
  318. item.remove();
  319. }
  320. });
  321.  
  322. // create suggestions
  323. forEach(data, function(videoData) {
  324. if (videoData.videoRenderer || videoData.compactVideoRenderer) {
  325. window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.videoRenderer || videoData.compactVideoRenderer, "ytd-compact-video-renderer"));
  326. } else if (videoData.radioRenderer || videoData.compactRadioRenderer) {
  327. window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.radioRenderer || videoData.compactRadioRenderer, "ytd-compact-radio-renderer"));
  328. } else if (videoData.playlistRenderer || videoData.compactPlaylistRenderer) {
  329. window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.playlistRenderer || videoData.compactPlaylistRenderer, "ytd-compact-playlist-renderer"));
  330. }
  331. });
  332.  
  333. if (hidden_continuation_item_renderer) {
  334. watchRelated.appendChild(hidden_continuation_item_renderer);
  335. }
  336.  
  337. script.searched = true;
  338.  
  339. return true;
  340. }
  341.  
  342. function resetSuggestions() {
  343. if (script.searched) {
  344. let itemSectionRenderer = document.querySelector('#related ytd-watch-next-secondary-results-renderer #items ytd-item-section-renderer') || document.querySelector("#related ytd-watch-next-secondary-results-renderer");
  345. let data = insp(itemSectionRenderer).__data.data;
  346. createSuggestions(data.contents || data.results);
  347.  
  348. // restore continuation renderer
  349. let continuation = itemSectionRenderer.querySelector('ytd-continuation-item-renderer[hidden]');
  350. if (continuation) {
  351. continuation.removeAttribute("hidden");
  352. }
  353. }
  354.  
  355. script.searched = false;
  356. }
  357.  
  358. function videoQueuePolymer(videoData, type) {
  359. let node = document.createElement(type);
  360. node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer", "yt-search-generated");
  361. node.data = videoData;
  362. return node;
  363. }
  364.  
  365. function injectCSS() {
  366. let css = `
  367. .autocomplete-suggestions {
  368. text-align: left; cursor: default; border: 1px solid var(--ytd-searchbox-legacy-border-color); border-top: 0; background: var(--ytd-searchbox-background);
  369. position: absolute; /*display: none; z-index: 9999;*/ max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box; box-shadow: -1px 1px 3px rgba(0,0,0,.1);
  370. left: auto; top: auto; width: 100%; margin: 0; contain: content; /* 1.2.0 */
  371. }
  372. .autocomplete-suggestion { position: relative; padding: 0 .6em; line-height: 23px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1.22em; color: var(--ytd-searchbox-text-color); }
  373. .autocomplete-suggestion b { font-weight: normal; color: #b31217; }
  374. .autocomplete-suggestion.selected { background: #ddd; }
  375. [dark] .autocomplete-suggestion.selected { background: #333; }
  376.  
  377. autocomplete-holder {
  378. overflow: visible; position: absolute; left: auto; top: auto; width: 100%; height: 0; z-index: 9999; box-sizing: border-box; margin:0; padding:0; border:0; contain: size layout;
  379. }
  380.  
  381. ytd-compact-autoplay-renderer { padding-bottom: 0px; }
  382.  
  383. #suggestions-search {
  384. outline: none; width: 100%; padding: 6px 5px; margin-bottom: 16px;
  385. border: 1px solid var(--ytd-searchbox-legacy-border-color); border-radius: 2px 0 0 2px;
  386. box-shadow: inset 0 1px 2px var(--ytd-searchbox-legacy-border-shadow-color);
  387. color: var(--ytd-searchbox-text-color); background-color: var(--ytd-searchbox-background);
  388. }
  389. `;
  390.  
  391. let style = document.createElement("style");
  392. style.type = "text/css";
  393. if (style.styleSheet){
  394. style.styleSheet.cssText = css;
  395. } else {
  396. style.appendChild(document.createTextNode(css));
  397. }
  398.  
  399. (document.body || document.head || document.documentElement).appendChild(style);
  400. }
  401.  
  402. // *** FUNCTIONALITY *** //
  403.  
  404. function forEach(array, callback, scope) {
  405. for (let i = 0; i < array.length; i++) {
  406. callback.call(scope, array[i], i);
  407. }
  408. }
  409.  
  410. // When you want to remove elements
  411. function forEachReverse(array, callback, scope) {
  412. for (let i = array.length - 1; i >= 0; i--) {
  413. callback.call(scope, array[i], i);
  414. }
  415. }
  416. }
  417.  
  418. // ================================================================================= //
  419. // =============================== INJECTING SCRIPTS =============================== //
  420. // ================================================================================= //
  421.  
  422. document.documentElement.setAttribute("youtube-search-while-watching-video", "");
  423.  
  424. if (!document.getElementById("autocomplete_script")) {
  425. let autoCompleteScript = document.createElement('script');
  426. autoCompleteScript.id = "autocomplete_script";
  427. autoCompleteScript.type = 'text/javascript';
  428. autoCompleteScript.textContent = 'window.autoComplete = ' + autoComplete + ';';
  429. (document.body || document.head || document.documentElement).appendChild(autoCompleteScript);
  430. }
  431.  
  432. if (!document.getElementById("search_while_watching_video")) {
  433. let searchWhileWatchingVideoScript = document.createElement('script');
  434. searchWhileWatchingVideoScript.id = "search_while_watching_video";
  435. searchWhileWatchingVideoScript.type = 'text/javascript';
  436. searchWhileWatchingVideoScript.textContent = '('+ youtube_search_while_watching_video +')();';
  437. (document.body || document.head || document.documentElement).appendChild(searchWhileWatchingVideoScript);
  438. }
  439. })();