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!

目前为 2021-07-04 提交的版本。查看 最新版本

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

QingJ © 2025

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