Youtube - Search While Watching Video

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

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址