Youtube Play Next Queue

Don't like the youtube autoplay suggestion? This script can create a queue with videos you want to play after your current video has finished!

  1. // ==UserScript==
  2. // @name Youtube Play Next Queue
  3. // @version 2.5.5
  4. // @description Don't like the youtube autoplay suggestion? This script can create a queue with videos you want to play after your current video has finished!
  5. // @author Cpt_mathix & CY Fung
  6. // @match https://www.youtube.com/*
  7. // @include https://www.youtube.com/*
  8. // @license GPL-2.0-or-later; http://www.gnu.org/licenses/gpl-2.0.txt
  9. // @require https://cdn.jsdelivr.net/gh/culefa/JavaScript-autoComplete@19203f30f148e2d9d810ece292b987abb157bbe0/auto-complete.min.js
  10. // @namespace https://greatest.deepsurf.us/users/16080
  11. // @run-at document-start
  12. // @grant none
  13. // @noframes
  14. // ==/UserScript==
  15.  
  16. /**
  17. *
  18. * The latest fixes to the script were contributed by [CY Fung](https://greatest.deepsurf.us/en/users/371179)
  19. *
  20. **/
  21.  
  22. /* jshint esversion: 11 */
  23.  
  24. (function() {
  25. 'use strict';
  26.  
  27. // ================================================================================= //
  28. // ============================ YOUTUBE PLAY NEXT QUEUE ============================ //
  29. // ================================================================================= //
  30.  
  31. function youtube_play_next_queue_modern() {
  32.  
  33. let script = {
  34. version: "2.0.0",
  35. initialized: false,
  36.  
  37. queue: null,
  38. ytplayer: null,
  39.  
  40. queue_visible: false,
  41. queue_rendered_observer: null,
  42. video_renderer_observer: null,
  43. playnext_data_observer: null,
  44.  
  45. debug: false
  46. };
  47.  
  48. const insp = o => o ? (o.polymerController || o.inst || o || null) : (o || null);
  49.  
  50. document.addEventListener("load", loadScript);
  51. document.addEventListener("DOMContentLoaded", initScript);
  52.  
  53. window.addEventListener("storage", function(event) {
  54. if (script.initialized && /YTQUEUE-MODERN#.*#QUEUE/.test(event.key)) {
  55. initQueue();
  56. displayQueue();
  57. }
  58. });
  59.  
  60. // reload script on page change using youtube polymer fire events
  61. window.addEventListener("yt-page-data-updated", function(event) {
  62. if (script.debug) { console.log("# page updated #"); }
  63. startScript(2);
  64. });
  65.  
  66. function initScript() {
  67. if (script.debug) { console.log("### Youtube Play Next Queue Initializing ###"); }
  68.  
  69. if (window.Polymer === undefined) {
  70. return;
  71. }
  72.  
  73. initQueue();
  74. injectCSS();
  75.  
  76. // TODO, better / more efficient alternative?
  77. setInterval(addThumbOverlayClickListeners, 250);
  78. setInterval(initThumbOverlays, 1000);
  79. setInterval(initVideoPreviewThumbOverlay, 1000);
  80.  
  81. if (script.debug) { console.log("### Youtube Play Next Queue Initialized ###"); }
  82. script.initialized = true;
  83. }
  84.  
  85. function loadScript() {
  86. startScript(5);
  87. }
  88.  
  89. function startScript(retry) {
  90. script.queue_visible = false;
  91.  
  92. if (script.initialized && isPlayerAvailable()) {
  93. if (script.debug) { console.log("videoplayer is available"); }
  94. if (script.debug) { console.log("ytplayer: ", script.ytplayer); }
  95.  
  96. if (script.ytplayer) {
  97. if (script.debug) { console.log("initializing queue"); }
  98. displayQueue();
  99.  
  100. if (script.debug) { console.log("initializing video statelistener"); }
  101. initVideoStateListener();
  102.  
  103. if (script.debug) { console.log("initializing playnext data observer"); }
  104. initPlayNextDataObserver();
  105. } else {
  106. hideQueue();
  107. }
  108. } else if (retry > 0) { // fix conflict with Youtube+ script
  109. setTimeout( function() {
  110. startScript(--retry);
  111. }, 1000);
  112. } else {
  113. if (script.debug) { console.log("videoplayer is unavailable"); }
  114. }
  115. }
  116.  
  117. // *** LISTENERS & OBSERVERS *** //
  118.  
  119. function initVideoStateListener() {
  120. if (!script.ytplayer.classList.contains('initialized-listeners')) {
  121. script.ytplayer.classList.add('initialized-listeners');
  122. script.ytplayer.addEventListener("onStateChange", handleVideoStateChanged);
  123. } else {
  124. if (script.debug) { console.log("statelistener already initialized"); }
  125. }
  126.  
  127. // run handler once to make sure queue is in sync
  128. handleVideoStateChanged(script.ytplayer.getPlayerState());
  129. }
  130.  
  131. function handleVideoStateChanged(videoState) {
  132. if (script.debug) { console.log("player state changed: " + videoState + "; queue empty: " + script.queue.isEmpty()); }
  133.  
  134. const FINISHED_STATE = 0;
  135. const PLAYING_STATE = 1;
  136. const PAUSED_STATE = 2;
  137. const BUFFERING_STATE = 3;
  138. const CUED_STATE = 5;
  139.  
  140. if (!script.queue.isEmpty()) {
  141. // dequeue video from the queue if it is currently playing
  142. if (script.ytplayer.getVideoData().video_id === script.queue.peek().id) {
  143. script.queue.dequeue();
  144. }
  145. }
  146.  
  147. let currentVideoIdFromUrl = getVideoIdFromUrl(window.location.href);
  148. if (videoState !== BUFFERING_STATE && isWatchPage() && !!currentVideoIdFromUrl && script.ytplayer.getVideoData().video_id !== currentVideoIdFromUrl && script.ytplayer.getVideoData().isListed) {
  149. if (script.debug) { console.log("Videoplayer not correctly loaded, LoadVideoById manually"); }
  150. script.ytplayer.loadVideoById(currentVideoIdFromUrl);
  151. script.ytplayer.playVideo();
  152. }
  153.  
  154. if ((videoState === PLAYING_STATE || videoState === PAUSED_STATE) && !script.queue.isEmpty() && !isPlaylist()) {
  155. if (script.debug) { console.log("SetAsNextVideo: HandleVideoStateChanged"); }
  156. script.queue.peek().setAsNextVideo();
  157. }
  158.  
  159. if (videoState === PAUSED_STATE) {
  160. // TODO: check if this works
  161. // Check for annoying "are you still watching" popup
  162. setTimeout(() => {
  163. let button = document.querySelector('yt-confirm-dialog-renderer #confirm-button');
  164. if (button && !!(button.offsetWidth || button.offsetHeight || button.getClientRects().length)) {
  165. if (script.debug) { console.log("### Clicking confirm button popup ###"); }
  166. button.click();
  167. }
  168. }, 1000);
  169. }
  170. }
  171.  
  172. function initQueueRenderedObserver() {
  173. if (script.queue_rendered_observer) {
  174. script.queue_rendered_observer.disconnect();
  175. }
  176.  
  177. // if the queue is completely rendered, mutationCount is equal to the queue size
  178. // => initialize queue button listeners for Play Now, Play Next and Remove
  179. let mutationCount = 0;
  180. script.queue_rendered_observer = new MutationObserver(function(mutations) {
  181. mutations.forEach(function(mutation) {
  182. mutationCount += mutation.addedNodes.length;
  183. if (mutationCount === script.queue.size()) {
  184. initQueueButtons();
  185. script.queue_rendered_observer.disconnect();
  186. }
  187. });
  188. });
  189.  
  190. let observable = document.querySelector('#youtube-play-next-queue-renderer > #contents');
  191. script.queue_rendered_observer.observe(observable, { childList: true });
  192. }
  193.  
  194. function initPlayNextDataObserver() {
  195. if (script.playnext_data_observer) {
  196. script.playnext_data_observer.disconnect();
  197. }
  198.  
  199. // If youtube updates the videoplayer with the autoplay suggestion,
  200. // replace it with the next video in our queue.
  201. script.playnext_data_observer = new MutationObserver(function(mutations) {
  202. if (!script.queue.isEmpty() && script.queue_visible) {
  203. if (isPlaylist()) {
  204. if (script.debug) { console.log("Play next observer triggered but found playlist, hiding current queue"); }
  205. hideQueue();
  206. } else {
  207. forEach(mutations, function(mutation) {
  208. if (mutation.attributeName === "href") {
  209. let nextVideoId = getVideoIdFromUrl(document.querySelector('.ytp-next-button').href);
  210. let nextQueueItem = script.queue.peek();
  211. if (nextQueueItem.id !== nextVideoId) {
  212. if (script.debug) { console.log("SetAsNextVideo: PlayNextDataObserver"); }
  213. nextQueueItem.setAsNextVideo();
  214. }
  215. }
  216. });
  217. }
  218. }
  219. });
  220.  
  221. let observable = document.querySelector('.ytp-next-button');
  222. script.playnext_data_observer.observe(observable, { attributes: true });
  223. }
  224.  
  225. // *** VIDEOPLAYER *** //
  226.  
  227. function getVideoPlayer() {
  228. return insp(document.getElementById('movie_player'));
  229. }
  230.  
  231. function isPlayerAvailable() {
  232. script.ytplayer = getVideoPlayer();
  233. return script.ytplayer !== null && !!script.ytplayer.getVideoData?.().video_id;
  234. }
  235.  
  236. function isPlaylist() {
  237. return !!script.ytplayer.getVideoStats().list || !document.querySelector('ytd-playlist-panel-renderer.ytd-watch-flexy[hidden]');
  238. }
  239.  
  240. function isPlayerMinimized() {
  241. return !!document.querySelector('ytd-miniplayer[active][enabled]');
  242. }
  243.  
  244. function isWatchPage() {
  245. return !!insp(document.querySelector('ytd-app')).__data.isWatchPage;
  246. }
  247.  
  248. function getVideoData(element) {
  249. let data = insp(element).__data.data;
  250.  
  251. if (data?.content) {
  252. return data.content.videoRenderer;
  253. } else {
  254. return data;
  255. }
  256. }
  257.  
  258. function getAutoplaySuggestion() {
  259. return document.querySelector('ytd-compact-autoplay-renderer ytd-compact-video-renderer') || document.querySelector('#related > ytd-watch-next-secondary-results-renderer ytd-compact-video-renderer');
  260. }
  261.  
  262. function getVideoIdFromUrl(url) {
  263. let o = null;
  264. try {
  265. o = new URL(url);
  266. } catch (e) { }
  267. let v = (o.searchParams ? o.searchParams.get('v') : '') || '';
  268. return v;
  269. }
  270.  
  271. // function getVideoInfoFromUrl(url, info) {
  272. // if (url.indexOf("?") === -1) {
  273. // return null;
  274. // }
  275.  
  276. // let urlVariables = url.split("?")[1].split("&");
  277.  
  278. // for(let i = 0; i < urlVariables.length; i++) {
  279. // let varName = urlVariables[i].split("=");
  280.  
  281. // if (varName[0] === info) {
  282. // return varName[1] === undefined ? null : varName[1];
  283. // }
  284. // }
  285. // }
  286.  
  287. // *** OBJECTS *** //
  288.  
  289. // QueueItem object
  290. class QueueItem {
  291. constructor(id, data, type) {
  292. this.id = id;
  293. this.data = data;
  294. this.type = type;
  295. }
  296.  
  297. getVideoLength() {
  298. if (this.data.lengthText) {
  299. return this.data.lengthText.simpleText;
  300. } else if (this.data.thumbnailOverlays && this.data.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer) {
  301. return this.data.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text.simpleText;
  302. } else {
  303. return "";
  304. }
  305. }
  306.  
  307. getSmallestThumb() {
  308. return this.data.thumbnail.thumbnails.reduce(function (thumb, currentSmallestThumb) {
  309. return (currentSmallestThumb.height * currentSmallestThumb.width < thumb.height * thumb.width) ? currentSmallestThumb : thumb;
  310. });
  311. }
  312.  
  313. getBiggestThumb() {
  314. return this.data.thumbnail.thumbnails.reduce(function (thumb, currentBiggestThumb) {
  315. return (currentBiggestThumb.height * currentBiggestThumb.width > thumb.height * thumb.width) ? currentBiggestThumb : thumb;
  316. });
  317. }
  318.  
  319. setAsNextVideo() {
  320. const PLAYING_STATE = 1;
  321. const PAUSED_STATE = 2;
  322.  
  323. if (isPlaylist()) { return; }
  324.  
  325. let currentVideoState = script.ytplayer.getPlayerState();
  326. if (currentVideoState !== PLAYING_STATE && currentVideoState !== PAUSED_STATE) {
  327. return;
  328. }
  329.  
  330. if (this.id === script.ytplayer.getVideoData().video_id) {
  331. return;
  332. }
  333.  
  334. if (script.debug) { console.log("changing next video"); }
  335.  
  336. // next video autoplay settings
  337. let watchNextData = insp(document.querySelector('ytd-player')).__data.watchNextData;
  338.  
  339. if (watchNextData && watchNextData.contents && watchNextData.contents.twoColumnWatchNextResults && watchNextData.playerOverlays && watchNextData.playerOverlays.playerOverlayRenderer) {
  340. if (watchNextData.contents.twoColumnWatchNextResults.playlist) {
  341. return;
  342. }
  343.  
  344. let watchNextEndScreenRenderer = watchNextData.playerOverlays.playerOverlayRenderer.endScreen.watchNextEndScreenRenderer;
  345. watchNextEndScreenRenderer.results[0].endScreenVideoRenderer = this.data;
  346. watchNextEndScreenRenderer.results[0].endScreenVideoRenderer.lengthInSeconds = hmsToSeconds(this.getVideoLength());
  347.  
  348. let playerOverlayAutoplayRenderer = watchNextData.playerOverlays.playerOverlayRenderer.autoplay.playerOverlayAutoplayRenderer;
  349. playerOverlayAutoplayRenderer.background.thumbnails = this.data.thumbnail.thumbnails;
  350. playerOverlayAutoplayRenderer.byline = this.data.longBylineText || this.data.shortBylineText;
  351. playerOverlayAutoplayRenderer.nextButton.buttonRenderer.navigationEndpoint = this.data.navigationEndpoint;
  352. playerOverlayAutoplayRenderer.videoId = this.data.videoId;
  353. playerOverlayAutoplayRenderer.videoTitle = this.data.title.simpleText || this.data.title.runs[0].text;
  354.  
  355. let autoplay = watchNextData.contents.twoColumnWatchNextResults.autoplay.autoplay;
  356. autoplay.sets[0].autoplayVideo.watchEndpoint.videoId = this.data.videoId;
  357.  
  358. let watchNextResponse = { "raw_watch_next_response" : watchNextData};
  359. script.ytplayer.updateVideoData(watchNextResponse);
  360.  
  361. if (!script.queue_visible) {
  362. displayQueue();
  363. }
  364. }
  365. }
  366.  
  367. clearBadges() {
  368. this.data.badges = [];
  369. }
  370.  
  371. addBadge(label, classes = []) {
  372. let badge = {
  373. "metadataBadgeRenderer": {
  374. "style": classes.join(" "),
  375. "label": label
  376. }
  377. };
  378.  
  379. this.data.badges.push(badge);
  380. }
  381.  
  382. toNode(classes = []) {
  383. let node = document.createElement("ytd-compact-video-renderer");
  384. node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer");
  385. classes.forEach(className => node.classList.add(className));
  386. node.data = this.data;
  387. return node;
  388. }
  389.  
  390. static fromDOM(element) {
  391. let data = Object.assign({}, getVideoData(element));
  392. data.navigationEndpoint.watchEndpoint = { "videoId": data.videoId };
  393. data.navigationEndpoint.commandMetadata = { "webCommandMetadata": { "url": "/watch?v=" + data.videoId, webPageType: "WEB_PAGE_TYPE_WATCH" } };
  394. data.shortBylineText = data.shortBylineText || { "runs": [ { "text": data.title.accessibility.accessibilityData.label } ] };
  395.  
  396. let id = data.videoId;
  397. let type = element.tagName.toLowerCase();
  398.  
  399. return new QueueItem(id, data, type);
  400. }
  401.  
  402. static fromJSON(json) {
  403. let data = json.data;
  404. let id = json.id;
  405. let type = json.type;
  406. return new QueueItem(id, data, type);
  407. }
  408. }
  409.  
  410. // Queue object
  411. class Queue {
  412. constructor() {
  413. this.queue = [];
  414. }
  415.  
  416. get() {
  417. return this.queue;
  418. }
  419.  
  420. set(queue) {
  421. this.queue = queue;
  422. setCache("QUEUE", queue);
  423. }
  424.  
  425. size() {
  426. return this.queue.length;
  427. }
  428.  
  429. isEmpty() {
  430. return this.size() === 0;
  431. }
  432.  
  433. contains(videoId) {
  434. for (let i = 0; i < this.queue.length; i++) {
  435. if (this.queue[i].id === videoId) {
  436. return true;
  437. }
  438. }
  439. return false;
  440. }
  441.  
  442. peek() {
  443. return this.queue[0];
  444. }
  445.  
  446. enqueue(item) {
  447. this.queue.push(item);
  448. this.update();
  449. this.show(250);
  450. }
  451.  
  452. dequeue() {
  453. let item = this.queue.shift();
  454. this.update();
  455. this.show(0);
  456. return item;
  457. }
  458.  
  459. remove(index) {
  460. this.queue.splice(index, 1);
  461. this.update();
  462. this.show(250);
  463. }
  464.  
  465. playNext(index) {
  466. let video = this.queue.splice(index, 1);
  467. this.queue.unshift(video[0]);
  468. this.update();
  469. this.show(0);
  470. }
  471.  
  472. playNow() {
  473. script.ytplayer.nextVideo(true);
  474. }
  475.  
  476. update() {
  477. setCache("QUEUE", this.get());
  478. if (script.debug) { console.log("updated queue: ", this.get().slice()); }
  479. }
  480.  
  481. show(delay) {
  482. setTimeout(function() {
  483. if (isPlayerAvailable()) {
  484. displayQueue();
  485. }
  486. }, delay);
  487. }
  488.  
  489. reset() {
  490. this.queue = [];
  491. this.update();
  492. this.show(0);
  493. }
  494. }
  495.  
  496. // *** QUEUE *** //
  497.  
  498. function initQueue() {
  499. script.queue = new Queue();
  500. let cachedQueue = getCache("QUEUE");
  501.  
  502. if (cachedQueue) {
  503. try {
  504. cachedQueue = cachedQueue.map(queueItem => QueueItem.fromJSON(queueItem));
  505. script.queue.set(cachedQueue);
  506. } catch(e) {
  507. setCache("QUEUE", script.queue.get());
  508. }
  509. } else {
  510. setCache("QUEUE", script.queue.get());
  511. }
  512. }
  513.  
  514. function displayQueue() {
  515. if (script.debug) { console.log("showing queue: ", script.queue.get()); }
  516.  
  517. script.queue_visible = true;
  518.  
  519. let queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
  520. if (!queue && isWatchPage()) {
  521. let anchor = document.querySelector('#related');
  522. if (anchor) {
  523. let node = document.createElement("ytd-item-section-renderer");
  524. node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer", "youtube-play-next-queue");
  525. node.id = "youtube-play-next-queue-renderer";
  526. window.Polymer.dom(anchor).insertBefore(node, anchor.firstChild);
  527. queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
  528. }
  529. } else if (!queue) {
  530. return;
  531. }
  532.  
  533. // clear current content
  534. queue.innerHTML = "";
  535.  
  536. initQueueRenderedObserver();
  537.  
  538. // don't show the queue on playlist pages
  539. if (isPlaylist()) {
  540. if (script.debug) { console.log("Playlist found, hiding queue"); }
  541. queue.parentNode.setAttribute("hidden", "");
  542. script.queue_visible = false;
  543. return;
  544. }
  545.  
  546. // display new queue
  547. if (!script.queue.isEmpty()) {
  548. queue.parentNode.removeAttribute("hidden", "");
  549.  
  550. let autoplay = document.querySelector('ytd-compact-autoplay-renderer #contents');
  551. if (autoplay) { autoplay.setAttribute("hidden", ""); }
  552.  
  553. forEach(script.queue.get(), function(item, index) {
  554. try {
  555. loadQueueItem(item, index, queue);
  556. } catch (ex) {
  557. console.log("Failed to display queue item", ex);
  558. }
  559. });
  560. } else {
  561. queue.parentNode.setAttribute("hidden", "");
  562.  
  563. let autoplay = document.querySelector('ytd-compact-autoplay-renderer #contents');
  564. if (autoplay) { autoplay.removeAttribute("hidden", ""); }
  565.  
  566. // restore autoplay suggestion in video player
  567. if (script.debug) { console.log("SetAsNextVideo: Restore suggestion"); }
  568. let autoplaySuggestion = getAutoplaySuggestion();
  569. if (autoplaySuggestion) {
  570. QueueItem.fromDOM(getAutoplaySuggestion()).setAsNextVideo();
  571. }
  572.  
  573. script.queue_visible = false;
  574. }
  575. }
  576.  
  577. function loadQueueItem(item, index, queueContents) {
  578. item.clearBadges();
  579. if (index === 0) {
  580. if (script.debug) { console.log("SetAsNextVideo: Load first queue item"); }
  581. item.setAsNextVideo();
  582. item.addBadge("Play Now", ["QUEUE_BUTTON", "QUEUE_PLAY_NOW"]);
  583. // item.addBadge("↓", ["QUEUE_BUTTON", "QUEUE_MOVE_DOWN"]);
  584. item.addBadge("Remove", ["QUEUE_BUTTON", "QUEUE_REMOVE"]);
  585. } else {
  586. item.addBadge("Play Next", ["QUEUE_BUTTON", "QUEUE_PLAY_NEXT"]);
  587. // item.addBadge("↑", ["QUEUE_BUTTON", "QUEUE_MOVE_UP"]);
  588. // item.addBadge("↓", ["QUEUE_BUTTON", "QUEUE_MOVE_DOWN"]);
  589. item.addBadge("Remove", ["QUEUE_BUTTON", "QUEUE_REMOVE"]);
  590. }
  591. window.Polymer.dom(queueContents).appendChild(item.toNode(["queue-item"]));
  592. }
  593.  
  594. function hideQueue() {
  595. script.queue_visible = false;
  596.  
  597. if (script.debug) { console.log("hiding queue"); }
  598.  
  599. let queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
  600. if (!queue) { return; }
  601.  
  602. openToast("Youtube Play Next Queue hidden while playlist, mix or native youtube queue is active.");
  603.  
  604. // clear current content
  605. queue.innerHTML = "";
  606. queue.parentNode.setAttribute("hidden", "");
  607. }
  608.  
  609. // The "remove queue and all its videos" button
  610. function initRemoveQueueButton(anchor) {
  611. let html = "<div class=\"queue-button remove-queue\">Remove Queue</div>";
  612. anchor.innerHTML = html;
  613.  
  614. if (!anchor.querySelector(".flex-whitebox")) {
  615. anchor.classList.add("flex-none");
  616. anchor.insertAdjacentHTML("afterend", "<div class=\"flex-whitebox\"></div>");
  617. }
  618.  
  619. anchor.querySelector('.remove-queue').addEventListener("click", function handler(e) {
  620. e.preventDefault();
  621. script.queue.reset();
  622. this.parentNode.innerHTML = "Up next";
  623. });
  624. }
  625.  
  626. // *** THUMB OVERLAYS *** //
  627.  
  628. function addThumbOverlay(thumbOverlays) {
  629. // we don't use the toggled icon, that's why both have the same values.
  630. let overlay = {
  631. "thumbnailOverlayToggleButtonRenderer": {
  632. "ytQueue": true,
  633. "isToggled": false,
  634. "toggledIcon": {iconType: "ADD"},
  635. "toggledTooltip": "Queue",
  636. "toggledAccessibility": {
  637. "accessibilityData": {
  638. "label": "Queue"
  639. }
  640. },
  641. "untoggledIcon": {iconType: "ADD"},
  642. "untoggledTooltip": "Queue",
  643. "untoggledAccessibility": {
  644. "accessibilityData": {
  645. "label": "Queue"
  646. }
  647. }
  648. }
  649. };
  650.  
  651. thumbOverlays.push(overlay);
  652. }
  653.  
  654. function hasThumbOverlay(videoOverlays) {
  655. for(let i = 0; i < videoOverlays.length; i++) {
  656. if (videoOverlays[i].thumbnailOverlayToggleButtonRenderer && videoOverlays[i].thumbnailOverlayToggleButtonRenderer.ytQueue) {
  657. return true;
  658. }
  659. }
  660. return false;
  661. }
  662.  
  663. function initThumbOverlay(videoRenderer) {
  664. let videoData = getVideoData(videoRenderer);
  665. if (videoData && videoData.thumbnailOverlays && !hasThumbOverlay(videoData.thumbnailOverlays) && !videoData.upcomingEventData) {
  666. addThumbOverlay(videoData.thumbnailOverlays);
  667. }
  668. }
  669.  
  670. function initThumbOverlays() {
  671. let videoRenderers = document.querySelectorAll('ytd-compact-video-renderer, ytd-grid-video-renderer, ytd-video-renderer, ytd-playlist-video-renderer, ytd-rich-grid-video-renderer, ytd-rich-item-renderer');
  672. forEach(videoRenderers, function(videoRenderer) {
  673. initThumbOverlay(videoRenderer);
  674.  
  675. // https://greatest.deepsurf.us/en/scripts/389174-yt-peek-a-pic
  676. detectPeekAPic(videoRenderer);
  677. });
  678. }
  679.  
  680. function detectPeekAPic(videoRenderer) {
  681. var peekAPicStoryboard = videoRenderer.querySelector(".yt-peek-a-pic-storyboard");
  682. if (!peekAPicStoryboard) return;
  683. videoRenderer.classList.add("peek-a-pic-enabled");
  684. }
  685.  
  686. function addThumbOverlayClickListeners() {
  687. let overlays = document.querySelectorAll('ytd-thumbnail-overlay-toggle-button-renderer > yt-icon');
  688.  
  689. forEach(overlays, function(overlay) {
  690. overlay.removeEventListener("click", handleThumbOverlayClick);
  691.  
  692. if (overlay.parentNode.getAttribute("aria-label") !== "Queue") {
  693. return;
  694. }
  695.  
  696. overlay.addEventListener("click", handleThumbOverlayClick);
  697. });
  698. }
  699.  
  700. function handleThumbOverlayClick(event) {
  701. event.stopPropagation(); event.preventDefault();
  702.  
  703. let path = event.path || (event.composedPath && event.composedPath()) || event._composedPath;
  704. for(let i = 0; i < path.length; i++) {
  705. let tagNames = ["YTD-COMPACT-VIDEO-RENDERER", "YTD-GRID-VIDEO-RENDERER", "YTD-VIDEO-RENDERER", "YTD-PLAYLIST-VIDEO-RENDERER", "YTD-RICH-GRID-VIDEO-RENDERER", "YTD-RICH-ITEM-RENDERER"];
  706. if (tagNames.includes(path[i].tagName)) {
  707. let newQueueItem = QueueItem.fromDOM(path[i]);
  708. if (!script.queue.contains(newQueueItem.id)) {
  709. script.queue.enqueue(newQueueItem);
  710.  
  711. if (script.queue_visible && (isWatchPage() || isPlayerMinimized())) {
  712. openToast("Video Added to Queue!", event.target);
  713. } else if (isPlaylist()) {
  714. openToast("Video Added to Queue! Queue is hidden while playlist, mix or native youtube queue is active", event.target);
  715. } else {
  716. openToast("Video Added to Queue! Play any video to view it.", event.target);
  717. }
  718. } else {
  719. openToast("Video Already Queued", event.target);
  720. }
  721. break;
  722. }
  723. }
  724. }
  725.  
  726. // *** VIDEO PREVIEW THUMB OVERLAYS *** //
  727.  
  728. function initVideoPreviewThumbOverlay() {
  729. let videoPreview = document.querySelector('ytd-video-preview');
  730. if (videoPreview) {
  731. let previewControls = videoPreview.querySelector(".ytp-inline-preview-controls");
  732. if (previewControls) {
  733. let queueControl = previewControls.querySelector("#queue-control-add");
  734. if (!queueControl) {
  735. previewControls.insertAdjacentHTML("beforeend", '<button id="queue-control-add" class="ytp-subtitles-button ytp-button" aria-keyshortcuts="m" data-title-no-tooltip="Queue" aria-label="Queue" title="Queue"><svg xmlns="http://www.w3.org/2000/svg" focusable="false" fill-opacity="1" width="100%" height="100%" viewBox="-5 -7 36 36"><path d="M20 12h-8v8h-1v-8H3v-1h8V3h1v8h8v1z" fill="#fff"></path></svg></button>');
  736. previewControls.querySelector("#queue-control-add").addEventListener("click", handlePreviewThumbOverlayClick);
  737. }
  738. }
  739. }
  740. }
  741.  
  742. function handlePreviewThumbOverlayClick(event) {
  743. event.stopPropagation(); event.preventDefault();
  744.  
  745. let videoPreview = document.querySelector('ytd-video-preview');
  746. if (videoPreview) {
  747. let mediaRenderer = insp(videoPreview).__data.opts.mediaRenderer;
  748. let newQueueItem = QueueItem.fromDOM(mediaRenderer);
  749. if (!script.queue.contains(newQueueItem.id)) {
  750. script.queue.enqueue(newQueueItem);
  751.  
  752. if (script.queue_visible && (isWatchPage() || isPlayerMinimized())) {
  753. openToast("Video Added to Queue!", event.target);
  754. } else if (isPlaylist()) {
  755. openToast("Video Added to Queue! Queue is hidden while playlist, mix or native youtube queue is active", event.target);
  756. } else {
  757. openToast("Video Added to Queue! Play any video to view it.", event.target);
  758. }
  759. } else {
  760. openToast("Video Already Queued", event.target);
  761. }
  762. }
  763. }
  764.  
  765. // *** BUTTONS *** //
  766.  
  767. function initQueueButtons() {
  768. // initQueueButtonAction("queue-play-now", () => script.queue.playNow());
  769. initQueueButtonAction("queue-play-next", (pos) => script.queue.playNext(pos+1));
  770. initQueueButtonAction("queue-remove", (pos) => script.queue.remove(pos));
  771. }
  772.  
  773. function initQueueButtonAction(className, btnAction) {
  774. let buttons = document.getElementsByClassName(className);
  775.  
  776. forEach(buttons, function(button, index) {
  777. let pos = index;
  778. if (!button.classList.contains("button-listener")) {
  779. button.addEventListener("click", function(event) {
  780. event.preventDefault();
  781. event.stopPropagation();
  782. btnAction(pos);
  783. });
  784. button.classList.add("button-listener");
  785. }
  786. });
  787. }
  788.  
  789. // *** POPUPS *** //
  790.  
  791. function openToast(text, target) {
  792. let openPopupAction = {
  793. "openPopupAction": {
  794. "popup": {
  795. "notificationActionRenderer": {
  796. "responseText": {simpleText: text},
  797. "trackingParams": ""
  798. }
  799. },
  800. "popupType": "TOAST"
  801. }
  802. };
  803.  
  804. let popupContainer = document.querySelector('ytd-popup-container');
  805. if (popupContainer.handleOpenPopupAction_) {
  806. popupContainer.handleOpenPopupAction_(openPopupAction, target || document.documentElement);
  807. } else {
  808. popupContainer.handleOpenPopupAction(openPopupAction, target || document.documentElement);
  809. }
  810. }
  811.  
  812. // *** LOCALSTORAGE *** //
  813.  
  814. function getCache(key) {
  815. return JSON.parse(localStorage.getItem("YTQUEUE-MODERN#" + script.version + "#" + key));
  816. }
  817.  
  818. function deleteCache(key) {
  819. localStorage.removeItem("YTQUEUE-MODERN#" + script.version + "#" + key);
  820. }
  821.  
  822. function setCache(key, value) {
  823. localStorage.setItem("YTQUEUE-MODERN#" + script.version + "#" + key, JSON.stringify(value));
  824. }
  825.  
  826. // *** CSS *** //
  827.  
  828. // injecting css
  829. function injectCSS() {
  830. let css = `
  831. #youtube-play-next-queue-renderer {
  832. height: 310px;
  833. position: sticky; /* needed for chrome to show resize handler */
  834. border: 1px solid var(--yt-spec-10-percent-layer);
  835. padding: 5px 0 0 5px;
  836. margin-bottom: 16px;
  837. overflow-y: visible;
  838. overflow-x: hidden;
  839. resize: vertical;
  840. }
  841.  
  842. ytd-compact-autoplay-renderer > #contents { padding-bottom: 8px }
  843.  
  844. .queue-item { margin-top: 0px !important; margin-bottom: 6px !important; }
  845. .queue-item #metadata-line { display: none; }
  846.  
  847. .queue-button { height: 15px; line-height: 1.7rem !important; padding: 5px !important; margin: 5px 3px !important; cursor: default; z-index: 99; background-color: var(--yt-spec-10-percent-layer); color: var(--yt-spec-text-secondary); }
  848. .queue-button.queue-play-now, .queue-button.queue-play-next { margin: 5px 3px 5px 0 !important; }
  849. .queue-button:hover { box-shadow: 0px 0px 3px black; }
  850. [dark] .queue-button:hover { box-shadow: 0px 0px 3px white; }
  851.  
  852. ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] { bottom: 0; top: auto !important; right: auto; left: 0; }
  853. .peek-a-pic-enabled ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] { bottom: 25%; top: auto !important; right: auto; left: 0; }
  854. ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] #label-container { left: 28px !important; right: auto !important; }
  855. ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] #label-container > #label { padding: 0 8px 0 2px !important; }
  856. ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] paper-tooltip { right: -70px !important; left: auto !important }
  857. .queue-item ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] { display: none; }
  858.  
  859. ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queued] { display: none; }
  860. `;
  861.  
  862. let style = document.createElement("style");
  863. style.type = "text/css";
  864. if (style.styleSheet){
  865. style.styleSheet.cssText = css;
  866. } else {
  867. style.appendChild(document.createTextNode(css));
  868. }
  869.  
  870. (document.body || document.head || document.documentElement).appendChild(style);
  871. }
  872.  
  873. // *** FUNCTIONALITY *** //
  874.  
  875. function forEach(array, callback, scope) {
  876. for (let i = 0; i < array.length; i++) {
  877. callback.call(scope, array[i], i);
  878. }
  879. }
  880.  
  881. // When you want to remove elements
  882. function forEachReverse(array, callback, scope) {
  883. for (let i = array.length - 1; i >= 0; i--) {
  884. callback.call(scope, array[i], i);
  885. }
  886. }
  887.  
  888. // hh:mm:ss => only seconds
  889. function hmsToSeconds(str) {
  890. let p = str.split(":"),
  891. s = 0, m = 1;
  892.  
  893. while (p.length > 0) {
  894. s += m * parseInt(p.pop(), 10);
  895. m *= 60;
  896. }
  897.  
  898. return s;
  899. }
  900. }
  901.  
  902. function youtube_search_while_watching_video() {
  903.  
  904. let script = {
  905. initialized: false,
  906.  
  907. ytplayer: null,
  908.  
  909. search_bar: null,
  910. search_autocomplete: null,
  911. search_suggestions: [],
  912. searched: false,
  913.  
  914. debug: false
  915. };
  916.  
  917. const insp = o => o ? (o.polymerController || o.inst || o || null) : (o || null);
  918.  
  919. document.addEventListener("DOMContentLoaded", initScript);
  920.  
  921. // reload script on page change using youtube polymer fire events
  922. window.addEventListener("yt-page-data-updated", function(event) {
  923. if (script.debug) { console.log("# page updated #"); }
  924. cleanupSearch();
  925. startScript(2);
  926. });
  927.  
  928. function initScript() {
  929. if (script.debug) { console.log("### Youtube Search While Watching Video Initializing ###"); }
  930.  
  931. initSearch();
  932. injectCSS();
  933.  
  934. if (script.debug) { console.log("### Youtube Search While Watching Video Initialized ###"); }
  935. script.initialized = true;
  936.  
  937. startScript(5);
  938. }
  939.  
  940. function startScript(retry) {
  941. if (script.initialized && isPlayerAvailable()) {
  942. if (script.debug) { console.log("videoplayer is available"); }
  943. if (script.debug) { console.log("ytplayer: ", script.ytplayer); }
  944.  
  945. if (script.ytplayer) {
  946. try {
  947. if (script.debug) { console.log("initializing search"); }
  948. loadSearch();
  949. } catch (error) {
  950. console.log("Failed to initialize search: ", (script.debug) ? error : error.message);
  951. }
  952. }
  953. } else if (retry > 0) { // fix conflict with Youtube+ script
  954. setTimeout( function() {
  955. startScript(--retry);
  956. }, 1000);
  957. } else {
  958. if (script.debug) { console.log("videoplayer is unavailable"); }
  959. }
  960. }
  961.  
  962. // *** VIDEOPLAYER *** //
  963.  
  964. function getVideoPlayer() {
  965. return insp(document.getElementById('movie_player'));
  966. }
  967.  
  968. function isPlayerAvailable() {
  969. script.ytplayer = getVideoPlayer();
  970. return script.ytplayer !== null && script.ytplayer.getVideoData?.().video_id;
  971. }
  972.  
  973. // *** SEARCH *** //
  974.  
  975. function initSearch() {
  976. // callback function for search suggestion results
  977. window.suggestions_callback = suggestionsCallback;
  978. }
  979.  
  980. function loadSearch() {
  981. // prevent double searchbar
  982. let playlistOrLiveSearchBar = document.querySelector('#suggestions-search.playlist-or-live');
  983. if (playlistOrLiveSearchBar) { playlistOrLiveSearchBar.remove(); }
  984.  
  985. let searchbar = document.getElementById('suggestions-search');
  986. if (!searchbar) {
  987. createSearchBar();
  988. } else {
  989. searchbar.value = "";
  990. }
  991.  
  992. script.searched = false;
  993. }
  994.  
  995. function cleanupSearch() {
  996. if (script.search_autocomplete) {
  997. script.search_autocomplete.destroy();
  998. }
  999.  
  1000. cleanupSuggestionRequests();
  1001. }
  1002.  
  1003. function createSearchBar() {
  1004. let anchor, html;
  1005.  
  1006. anchor = document.querySelector('ytd-compact-autoplay-renderer > #contents');
  1007. if (anchor) {
  1008. html = "<input id=\"suggestions-search\" type=\"search\" placeholder=\"Search\">";
  1009. anchor.insertAdjacentHTML("afterend", html);
  1010. } else { // playlist, live video or experimental youtube layout (where autoplay is not a separate renderer anymore)
  1011. anchor = document.querySelector('#related > ytd-watch-next-secondary-results-renderer');
  1012. if (anchor) {
  1013. html = "<input id=\"suggestions-search\" class=\"playlist-or-live\" type=\"search\" placeholder=\"Search\">";
  1014. anchor.insertAdjacentHTML("beforebegin", html);
  1015. }
  1016. }
  1017.  
  1018. let searchBar = document.getElementById('suggestions-search');
  1019. if (searchBar) {
  1020. script.search_bar = searchBar;
  1021.  
  1022. script.search_autocomplete = new window.autoComplete({
  1023. selector: '#suggestions-search',
  1024. minChars: 1,
  1025. delay: 100,
  1026. source: function(term, suggest) {
  1027. script.search_suggestions = {
  1028. query: term,
  1029. suggest: suggest
  1030. };
  1031. searchSuggestions(term);
  1032. },
  1033. onSelect: function(event, term, item) {
  1034. prepareNewSearchRequest(term);
  1035. }
  1036. });
  1037.  
  1038. script.search_bar.addEventListener("keyup", function(event) {
  1039. if (this.value === "") {
  1040. resetSuggestions();
  1041. }
  1042. });
  1043.  
  1044. // seperate keydown listener because the search listener blocks keyup..?
  1045. script.search_bar.addEventListener("keydown", function(event) {
  1046. const ENTER = 13;
  1047. if (this.value.trim() !== "" && (event.key == "Enter" || event.keyCode === ENTER)) {
  1048. prepareNewSearchRequest(this.value.trim());
  1049. }
  1050. });
  1051.  
  1052. script.search_bar.addEventListener("search", function(event) {
  1053. if(this.value === "") {
  1054. script.search_bar.blur(); // close search suggestions dropdown
  1055. script.search_suggestions = []; // clearing the search suggestions
  1056.  
  1057. resetSuggestions();
  1058. }
  1059. });
  1060.  
  1061. script.search_bar.addEventListener("focus", function(event) {
  1062. this.select();
  1063. });
  1064. }
  1065. }
  1066.  
  1067. // callback from search suggestions attached to window
  1068. function suggestionsCallback(data) {
  1069. if (script.debug) { console.log(data); }
  1070.  
  1071. let query = data[0];
  1072. if (query !== script.search_suggestions.query) {
  1073. return;
  1074. }
  1075.  
  1076. let raw = data[1]; // extract relevant data from json
  1077. let suggestions = raw.map(function(array) {
  1078. return array[0]; // change 2D array to 1D array with only suggestions
  1079. });
  1080.  
  1081. script.search_suggestions.suggest(suggestions);
  1082. }
  1083.  
  1084. function searchSuggestions(query) {
  1085. // youtube search parameters
  1086. const GeoLocation = window.yt.config_.INNERTUBE_CONTEXT_GL;
  1087. const HostLanguage = window.yt.config_.INNERTUBE_CONTEXT_HL;
  1088.  
  1089. if (script.debug) { console.log("suggestion request send", query); }
  1090. let scriptElement = document.createElement("script");
  1091. scriptElement.type = "text/javascript";
  1092. scriptElement.className = "suggestion-request";
  1093. 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";
  1094. (document.body || document.head || document.documentElement).appendChild(scriptElement);
  1095. }
  1096.  
  1097. function cleanupSuggestionRequests() {
  1098. let requests = document.getElementsByClassName('suggestion-request');
  1099. forEachReverse(requests, function(request) {
  1100. request.remove();
  1101. });
  1102. }
  1103.  
  1104. // send new search request (with the search bar)
  1105. function prepareNewSearchRequest(value) {
  1106. if (script.debug) { console.log("searching for " + value); }
  1107.  
  1108. script.search_bar.blur(); // close search suggestions dropdown
  1109. script.search_suggestions = []; // clearing the search suggestions
  1110. cleanupSuggestionRequests();
  1111.  
  1112. sendSearchRequest("https://www.youtube.com/results?pbj=1&search_query=" + encodeURIComponent(value));
  1113. }
  1114.  
  1115. // given the url, retrieve the search results
  1116. function sendSearchRequest(url) {
  1117. let xmlHttp = new XMLHttpRequest();
  1118. xmlHttp.onreadystatechange = function() {
  1119. if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
  1120. processSearch(xmlHttp.responseText);
  1121. }
  1122. };
  1123.  
  1124. xmlHttp.open("GET", url, true);
  1125. xmlHttp.setRequestHeader("x-youtube-client-name", window.yt.config_.INNERTUBE_CONTEXT_CLIENT_NAME);
  1126. xmlHttp.setRequestHeader("x-youtube-client-version", window.yt.config_.INNERTUBE_CONTEXT_CLIENT_VERSION);
  1127. xmlHttp.setRequestHeader("x-youtube-client-utc-offset", new Date().getTimezoneOffset() * -1);
  1128.  
  1129. if (window.yt.config_.ID_TOKEN) { // null if not logged in
  1130. xmlHttp.setRequestHeader("x-youtube-identity-token", window.yt.config_.ID_TOKEN);
  1131. }
  1132.  
  1133. xmlHttp.send(null);
  1134. }
  1135.  
  1136. // process search request
  1137. function processSearch(responseText) {
  1138. try {
  1139. let data = JSON.parse(responseText);
  1140.  
  1141. let found = searchJson(data, (key, value) => {
  1142. if (key === "itemSectionRenderer") {
  1143. if (script.debug) { console.log(value.contents); }
  1144. let succeeded = createSuggestions(value.contents);
  1145. return succeeded;
  1146. }
  1147. return false;
  1148. });
  1149.  
  1150. if (!found) {
  1151. alert("The search request was succesful but the script was unable to parse the results");
  1152. }
  1153. } catch (error) {
  1154. alert("Failed to retrieve search data, sorry!\nError message: " + error.message + "\nSearch response: " + responseText);
  1155. }
  1156. }
  1157.  
  1158. function searchJson(json, func) {
  1159. let found = false;
  1160.  
  1161. for (let item in json) {
  1162. found = func(item, json[item]);
  1163. if (found) { break; }
  1164.  
  1165. if (json[item] !== null && typeof(json[item]) == "object") {
  1166. found = searchJson(json[item], func);
  1167. if (found) { break; }
  1168. }
  1169. }
  1170.  
  1171. return found;
  1172. }
  1173.  
  1174. // *** HTML & CSS *** //
  1175.  
  1176. function createSuggestions(data) {
  1177. // filter out promotional stuff
  1178. if (data.length < 10) {
  1179. return false;
  1180. }
  1181.  
  1182. // remove current suggestions
  1183. let hidden_continuation_item_renderer;
  1184. 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');
  1185. forEachReverse(watchRelated.children, function(item) {
  1186. if (item.tagName === "YTD-CONTINUATION-ITEM-RENDERER") {
  1187. item.setAttribute("hidden", "");
  1188. hidden_continuation_item_renderer = item;
  1189. } else if (item.tagName !== "YTD-COMPACT-AUTOPLAY-RENDERER") {
  1190. item.remove();
  1191. }
  1192. });
  1193.  
  1194. // create suggestions
  1195. forEach(data, function(videoData) {
  1196. if (videoData.videoRenderer || videoData.compactVideoRenderer) {
  1197. window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.videoRenderer || videoData.compactVideoRenderer, "ytd-compact-video-renderer"));
  1198. } else if (videoData.radioRenderer || videoData.compactRadioRenderer) {
  1199. window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.radioRenderer || videoData.compactRadioRenderer, "ytd-compact-radio-renderer"));
  1200. } else if (videoData.playlistRenderer || videoData.compactPlaylistRenderer) {
  1201. window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.playlistRenderer || videoData.compactPlaylistRenderer, "ytd-compact-playlist-renderer"));
  1202. }
  1203. });
  1204.  
  1205. if (hidden_continuation_item_renderer) {
  1206. watchRelated.appendChild(hidden_continuation_item_renderer);
  1207. }
  1208.  
  1209. script.searched = true;
  1210.  
  1211. return true;
  1212. }
  1213.  
  1214. function resetSuggestions() {
  1215. if (script.searched) {
  1216. 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");
  1217. let data = insp(itemSectionRenderer).__data.data;
  1218. createSuggestions(data.contents || data.results);
  1219.  
  1220. // restore continuation renderer
  1221. let continuation = itemSectionRenderer.querySelector('ytd-continuation-item-renderer[hidden]');
  1222. if (continuation) {
  1223. continuation.removeAttribute("hidden");
  1224. }
  1225. }
  1226.  
  1227. script.searched = false;
  1228. }
  1229.  
  1230. function videoQueuePolymer(videoData, type) {
  1231. let node = document.createElement(type);
  1232. node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer", "yt-search-generated");
  1233. node.data = videoData;
  1234. return node;
  1235. }
  1236.  
  1237. function injectCSS() {
  1238. let css = `
  1239. .autocomplete-suggestions {
  1240. text-align: left; cursor: default; border: 1px solid var(--ytd-searchbox-legacy-border-color); border-top: 0; background: var(--ytd-searchbox-background);
  1241. 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);
  1242. left: auto; top: auto; width: 100%; margin: 0; contain: content; /* 1.2.0 */
  1243. }
  1244. .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); }
  1245. .autocomplete-suggestion b { font-weight: normal; color: #b31217; }
  1246. .autocomplete-suggestion.selected { background: #ddd; }
  1247. [dark] .autocomplete-suggestion.selected { background: #333; }
  1248.  
  1249. autocomplete-holder {
  1250. 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;
  1251. }
  1252.  
  1253. ytd-compact-autoplay-renderer { padding-bottom: 0px; }
  1254.  
  1255. #suggestions-search {
  1256. outline: none; width: 100%; padding: 6px 5px; margin-bottom: 16px;
  1257. border: 1px solid var(--ytd-searchbox-legacy-border-color); border-radius: 2px 0 0 2px;
  1258. box-shadow: inset 0 1px 2px var(--ytd-searchbox-legacy-border-shadow-color);
  1259. color: var(--ytd-searchbox-text-color); background-color: var(--ytd-searchbox-background);
  1260. }
  1261. `;
  1262.  
  1263. let style = document.createElement("style");
  1264. style.type = "text/css";
  1265. if (style.styleSheet){
  1266. style.styleSheet.cssText = css;
  1267. } else {
  1268. style.appendChild(document.createTextNode(css));
  1269. }
  1270.  
  1271. (document.body || document.head || document.documentElement).appendChild(style);
  1272. }
  1273.  
  1274. // *** FUNCTIONALITY *** //
  1275.  
  1276. function forEach(array, callback, scope) {
  1277. for (let i = 0; i < array.length; i++) {
  1278. callback.call(scope, array[i], i);
  1279. }
  1280. }
  1281.  
  1282. // When you want to remove elements
  1283. function forEachReverse(array, callback, scope) {
  1284. for (let i = array.length - 1; i >= 0; i--) {
  1285. callback.call(scope, array[i], i);
  1286. }
  1287. }
  1288. }
  1289.  
  1290. // ================================================================================= //
  1291. // =============================== INJECTING SCRIPTS =============================== //
  1292. // ================================================================================= //
  1293.  
  1294. document.documentElement.setAttribute("youtube-play-next-queue", "");
  1295.  
  1296. if (!document.getElementById("autocomplete_script")) {
  1297. let autoCompleteScript = document.createElement('script');
  1298. autoCompleteScript.id = "autocomplete_script";
  1299. autoCompleteScript.appendChild(document.createTextNode('window.autoComplete = ' + autoComplete + ';'));
  1300. (document.body || document.head || document.documentElement).appendChild(autoCompleteScript);
  1301. }
  1302.  
  1303. if (!document.getElementById("play_next_queue_script")) {
  1304. let playNextQueueScript = document.createElement('script');
  1305. playNextQueueScript.id = "play_next_queue_script";
  1306. playNextQueueScript.appendChild(document.createTextNode('('+ youtube_play_next_queue_modern +')();'));
  1307. (document.body || document.head || document.documentElement).appendChild(playNextQueueScript);
  1308. }
  1309.  
  1310. if (!document.getElementById("search_while_watching_video")) {
  1311. let searchWhileWatchingVideoScript = document.createElement('script');
  1312. searchWhileWatchingVideoScript.id = "search_while_watching_video";
  1313. searchWhileWatchingVideoScript.appendChild(document.createTextNode('('+ youtube_search_while_watching_video +')();'));
  1314. (document.body || document.head || document.documentElement).appendChild(searchWhileWatchingVideoScript);
  1315. }
  1316. })();