- // ==UserScript==
- // @name Youtube Play Next Queue
- // @version 2.4.0
- // @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!
- // @author Cpt_mathix
- // @match https://www.youtube.com/*
- // @include https://www.youtube.com/*
- // @license GPL-2.0-or-later; http://www.gnu.org/licenses/gpl-2.0.txt
- // @require https://cdnjs.cloudflare.com/ajax/libs/JavaScript-autoComplete/1.0.4/auto-complete.min.js
- // @namespace https://gf.qytechs.cn/users/16080
- // @run-at document-start
- // @grant none
- // @noframes
- // ==/UserScript==
-
- /* jshint esversion: 6 */
-
- (function() {
- 'use strict';
-
- // ================================================================================= //
- // ============================ YOUTUBE PLAY NEXT QUEUE ============================ //
- // ================================================================================= //
-
- function youtube_play_next_queue_modern() {
-
- let script = {
- version: "2.0.0",
- initialized: false,
-
- queue: null,
- ytplayer: null,
-
- queue_visible: false,
- queue_rendered_observer: null,
- video_renderer_observer: null,
- playnext_data_observer: null,
-
- debug: false
- };
-
- document.addEventListener("load", loadScript);
- document.addEventListener("DOMContentLoaded", initScript);
-
- window.addEventListener("storage", function(event) {
- if (script.initialized && /YTQUEUE-MODERN#.*#QUEUE/.test(event.key)) {
- initQueue();
- displayQueue();
- }
- });
-
- // reload script on page change using youtube polymer fire events
- window.addEventListener("yt-page-data-updated", function(event) {
- if (script.debug) { console.log("# page updated #"); }
- startScript(2);
- });
-
- function initScript() {
- if (script.debug) { console.log("### Youtube Play Next Queue Initializing ###"); }
-
- if (window.Polymer === undefined) {
- return;
- }
-
- initQueue();
- injectCSS();
-
- // TODO, better / more efficient alternative?
- setInterval(addThumbOverlayClickListeners, 250);
- setInterval(initThumbOverlays, 1000);
-
- if (script.debug) { console.log("### Youtube Play Next Queue Initialized ###"); }
- script.initialized = true;
- }
-
- function loadScript() {
- startScript(5);
- }
-
- function startScript(retry) {
- script.queue_visible = false;
-
- if (script.initialized && isPlayerAvailable()) {
- if (script.debug) { console.log("videoplayer is available"); }
- if (script.debug) { console.log("ytplayer: ", script.ytplayer); }
-
- if (script.ytplayer) {
- if (script.debug) { console.log("initializing queue"); }
- displayQueue();
-
- if (script.debug) { console.log("initializing video statelistener"); }
- initVideoStateListener();
-
- if (script.debug) { console.log("initializing playnext data observer"); }
- initPlayNextDataObserver();
- } else {
- hideQueue();
- }
- } else if (retry > 0) { // fix conflict with Youtube+ script
- setTimeout( function() {
- startScript(--retry);
- }, 1000);
- } else {
- if (script.debug) { console.log("videoplayer is unavailable"); }
- }
- }
-
- // *** LISTENERS & OBSERVERS *** //
-
- function initVideoStateListener() {
- if (!script.ytplayer.classList.contains('initialized-listeners')) {
- script.ytplayer.classList.add('initialized-listeners');
- script.ytplayer.addEventListener("onStateChange", handleVideoStateChanged);
- } else {
- if (script.debug) { console.log("statelistener already initialized"); }
- }
-
- // run handler once to make sure queue is in sync
- handleVideoStateChanged(script.ytplayer.getPlayerState());
- }
-
- function handleVideoStateChanged(videoState) {
- if (script.debug) { console.log("player state changed: " + videoState + "; queue empty: " + script.queue.isEmpty()); }
-
- const FINISHED_STATE = 0;
- const PLAYING_STATE = 1;
- const PAUSED_STATE = 2;
- const BUFFERING_STATE = 3;
- const CUED_STATE = 5;
-
- if (!script.queue.isEmpty()) {
- // dequeue video from the queue if it is currently playing
- if (script.ytplayer.getVideoData().video_id === script.queue.peek().id) {
- script.queue.dequeue();
- }
- }
-
- let currentVideoIdFromUrl = getVideoInfoFromUrl(window.location.href, "v");
- if (videoState !== BUFFERING_STATE && isWatchPage() && !!currentVideoIdFromUrl && script.ytplayer.getVideoData().video_id !== currentVideoIdFromUrl && script.ytplayer.getVideoData().isListed) {
- if (script.debug) { console.log("Videoplayer not correctly loaded, LoadVideoById manually"); }
- script.ytplayer.loadVideoById(currentVideoIdFromUrl);
- script.ytplayer.playVideo();
- }
-
- if ((videoState === PLAYING_STATE || videoState === PAUSED_STATE) && !script.queue.isEmpty() && !isPlaylist()) {
- if (script.debug) { console.log("SetAsNextVideo: HandleVideoStateChanged"); }
- script.queue.peek().setAsNextVideo();
- }
-
- if (videoState === PAUSED_STATE) {
- // TODO: check if this works
- // Check for annoying "are you still watching" popup
- setTimeout(() => {
- let button = document.querySelector('yt-confirm-dialog-renderer #confirm-button');
- if (button && !!(button.offsetWidth || button.offsetHeight || button.getClientRects().length)) {
- if (script.debug) { console.log("### Clicking confirm button popup ###"); }
- button.click();
- }
- }, 1000);
- }
- }
-
- function initQueueRenderedObserver() {
- if (script.queue_rendered_observer) {
- script.queue_rendered_observer.disconnect();
- }
-
- // if the queue is completely rendered, mutationCount is equal to the queue size
- // => initialize queue button listeners for Play Now, Play Next and Remove
- let mutationCount = 0;
- script.queue_rendered_observer = new MutationObserver(function(mutations) {
- mutations.forEach(function(mutation) {
- mutationCount += mutation.addedNodes.length;
- if (mutationCount === script.queue.size()) {
- initQueueButtons();
- script.queue_rendered_observer.disconnect();
- }
- });
- });
-
- let observable = document.querySelector('#youtube-play-next-queue-renderer > #contents');
- script.queue_rendered_observer.observe(observable, { childList: true });
- }
-
- function initPlayNextDataObserver() {
- if (script.playnext_data_observer) {
- script.playnext_data_observer.disconnect();
- }
-
- // If youtube updates the videoplayer with the autoplay suggestion,
- // replace it with the next video in our queue.
- script.playnext_data_observer = new MutationObserver(function(mutations) {
- if (!script.queue.isEmpty() && script.queue_visible) {
- if (isPlaylist()) {
- if (script.debug) { console.log("Play next observer triggered but found playlist, hiding current queue"); }
- hideQueue();
- } else {
- forEach(mutations, function(mutation) {
- if (mutation.attributeName === "href") {
- let nextVideoId = getVideoInfoFromUrl(document.querySelector('.ytp-next-button').href, "v");
- let nextQueueItem = script.queue.peek();
- if (nextQueueItem.id !== nextVideoId) {
- if (script.debug) { console.log("SetAsNextVideo: PlayNextDataObserver"); }
- nextQueueItem.setAsNextVideo();
- }
- }
- });
- }
- }
- });
-
- let observable = document.querySelector('.ytp-next-button');
- script.playnext_data_observer.observe(observable, { attributes: true });
- }
-
- // *** VIDEOPLAYER *** //
-
- function getVideoPlayer() {
- return document.getElementById('movie_player');
- }
-
- function isPlayerAvailable() {
- script.ytplayer = getVideoPlayer();
- return script.ytplayer !== null && !!script.ytplayer.getVideoData().video_id;
- }
-
- function isPlaylist() {
- return !!script.ytplayer.getVideoStats().list || !document.querySelector('ytd-playlist-panel-renderer.ytd-watch-flexy[hidden]');
- }
-
- function isLivePlayer() {
- return script.ytplayer.getVideoData().isLive;
- }
-
- function isPlayerFullscreen() {
- return script.ytplayer.classList.contains('ytp-fullscreen');
- }
-
- function isPlayerMinimized() {
- return !!document.querySelector('ytd-miniplayer[active][enabled]');
- }
-
- function isWatchPage() {
- return !!document.querySelector('ytd-app').__data.isWatchPage;
- }
-
- function getVideoData(element) {
- let data = element.__data.data;
-
- if (data.content) {
- return data.content.videoRenderer;
- } else {
- return data;
- }
- }
-
- function getAutoplaySuggestion() {
- return document.querySelector('ytd-compact-autoplay-renderer ytd-compact-video-renderer') || document.querySelector('#related > ytd-watch-next-secondary-results-renderer ytd-compact-video-renderer');
- }
-
- function getVideoInfoFromUrl(url, info) {
- if (url.indexOf("?") === -1) {
- return null;
- }
-
- let urlVariables = url.split("?")[1].split("&");
-
- for(let i = 0; i < urlVariables.length; i++) {
- let varName = urlVariables[i].split("=");
-
- if (varName[0] === info) {
- return varName[1] === undefined ? null : varName[1];
- }
- }
- }
-
- // *** OBJECTS *** //
-
- // QueueItem object
- class QueueItem {
- constructor(id, data, type) {
- this.id = id;
- this.data = data;
- this.type = type;
- }
-
- getVideoLength() {
- if (this.data.lengthText) {
- return this.data.lengthText.simpleText;
- } else if (this.data.thumbnailOverlays && this.data.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer) {
- return this.data.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text.simpleText;
- } else {
- return "";
- }
- }
-
- getSmallestThumb() {
- return this.data.thumbnail.thumbnails.reduce(function (thumb, currentSmallestThumb) {
- return (currentSmallestThumb.height * currentSmallestThumb.width < thumb.height * thumb.width) ? currentSmallestThumb : thumb;
- });
- }
-
- getBiggestThumb() {
- return this.data.thumbnail.thumbnails.reduce(function (thumb, currentBiggestThumb) {
- return (currentBiggestThumb.height * currentBiggestThumb.width > thumb.height * thumb.width) ? currentBiggestThumb : thumb;
- });
- }
-
- setAsNextVideo() {
- const PLAYING_STATE = 1;
- const PAUSED_STATE = 2;
-
- if (isPlaylist()) { return; }
-
- let currentVideoState = script.ytplayer.getPlayerState();
- if (currentVideoState !== PLAYING_STATE && currentVideoState !== PAUSED_STATE) {
- return;
- }
-
- if (this.id === script.ytplayer.getVideoData().video_id) {
- return;
- }
-
- if (script.debug) { console.log("changing next video"); }
-
- // next video autoplay settings
- let watchNextData = document.querySelector('ytd-player').__data.watchNextData;
- let watchNextResponse = { "raw_watch_next_response" : watchNextData};
-
- if (watchNextData.contents.twoColumnWatchNextResults.playlist) {
- return;
- }
-
- let watchNextEndScreenRenderer = watchNextData.playerOverlays.playerOverlayRenderer.endScreen.watchNextEndScreenRenderer;
- watchNextEndScreenRenderer.results[0].endScreenVideoRenderer = this.data;
- watchNextEndScreenRenderer.results[0].endScreenVideoRenderer.lengthInSeconds = hmsToSeconds(this.getVideoLength());
-
- let playerOverlayAutoplayRenderer = watchNextData.playerOverlays.playerOverlayRenderer.autoplay.playerOverlayAutoplayRenderer;
- playerOverlayAutoplayRenderer.background.thumbnails = this.data.thumbnail.thumbnails;
- playerOverlayAutoplayRenderer.byline = this.data.longBylineText || this.data.shortBylineText;
- playerOverlayAutoplayRenderer.nextButton.buttonRenderer.navigationEndpoint = this.data.navigationEndpoint;
- playerOverlayAutoplayRenderer.videoId = this.data.videoId;
- playerOverlayAutoplayRenderer.videoTitle = this.data.title.simpleText || this.data.title.runs[0].text;
-
- let autoplay = watchNextData.contents.twoColumnWatchNextResults.autoplay.autoplay;
- autoplay.sets[0].autoplayVideo.watchEndpoint.videoId = this.data.videoId;
-
- script.ytplayer.updateVideoData(watchNextResponse);
-
- if (!script.queue_visible) {
- displayQueue();
- }
- }
-
- clearBadges() {
- this.data.badges = [];
- }
-
- addBadge(label, classes = []) {
- let badge = {
- "metadataBadgeRenderer": {
- "style": classes.join(" "),
- "label": label
- }
- };
-
- this.data.badges.push(badge);
- }
-
- toNode(classes = []) {
- let node = document.createElement("ytd-compact-video-renderer");
- node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer");
- classes.forEach(className => node.classList.add(className));
- node.data = this.data;
- return node;
- }
-
- static fromDOM(element) {
- let data = Object.assign({}, getVideoData(element));
- data.navigationEndpoint.watchEndpoint = { "videoId": data.videoId };
- data.navigationEndpoint.commandMetadata = { "webCommandMetadata": { "url": "/watch?v=" + data.videoId, webPageType: "WEB_PAGE_TYPE_WATCH" } };
- data.shortBylineText = data.shortBylineText || { "runs": [ { "text": data.title.accessibility.accessibilityData.label } ] };
-
- let id = data.videoId;
- let type = element.tagName.toLowerCase();
-
- return new QueueItem(id, data, type);
- }
-
- static fromJSON(json) {
- let data = json.data;
- let id = json.id;
- let type = json.type;
- return new QueueItem(id, data, type);
- }
- }
-
- // Queue object
- class Queue {
- constructor() {
- this.queue = [];
- }
-
- get() {
- return this.queue;
- }
-
- set(queue) {
- this.queue = queue;
- setCache("QUEUE", queue);
- }
-
- size() {
- return this.queue.length;
- }
-
- isEmpty() {
- return this.size() === 0;
- }
-
- contains(videoId) {
- for (let i = 0; i < this.queue.length; i++) {
- if (this.queue[i].id === videoId) {
- return true;
- }
- }
- return false;
- }
-
- peek() {
- return this.queue[0];
- }
-
- enqueue(item) {
- this.queue.push(item);
- this.update();
- this.show(250);
- }
-
- dequeue() {
- let item = this.queue.shift();
- this.update();
- this.show(0);
- return item;
- }
-
- remove(index) {
- this.queue.splice(index, 1);
- this.update();
- this.show(250);
- }
-
- playNext(index) {
- let video = this.queue.splice(index, 1);
- this.queue.unshift(video[0]);
- this.update();
- this.show(0);
- }
-
- playNow() {
- script.ytplayer.nextVideo(true);
- }
-
- update() {
- setCache("QUEUE", this.get());
- if (script.debug) { console.log("updated queue: ", this.get().slice()); }
- }
-
- show(delay) {
- setTimeout(function() {
- if (isPlayerAvailable()) {
- displayQueue();
- }
- }, delay);
- }
-
- reset() {
- this.queue = [];
- this.update();
- this.show(0);
- }
- }
-
- // *** QUEUE *** //
-
- function initQueue() {
- script.queue = new Queue();
- let cachedQueue = getCache("QUEUE");
-
- if (cachedQueue) {
- try {
- cachedQueue = cachedQueue.map(queueItem => QueueItem.fromJSON(queueItem));
- script.queue.set(cachedQueue);
- } catch(e) {
- setCache("QUEUE", script.queue.get());
- }
- } else {
- setCache("QUEUE", script.queue.get());
- }
- }
-
- function displayQueue() {
- if (script.debug) { console.log("showing queue: ", script.queue.get()); }
-
- script.queue_visible = true;
-
- let queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
- if (!queue && isWatchPage()) {
- let anchor = document.querySelector('#related');
- if (anchor) {
- let node = document.createElement("ytd-item-section-renderer");
- node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer", "youtube-play-next-queue");
- node.id = "youtube-play-next-queue-renderer";
- window.Polymer.dom(anchor).insertBefore(node, anchor.firstChild);
- queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
- }
- } else if (!queue) {
- return;
- }
-
- // clear current content
- queue.innerHTML = "";
-
- initQueueRenderedObserver();
-
- // don't show the queue on playlist pages
- if (isPlaylist()) {
- if (script.debug) { console.log("Playlist found, hiding queue"); }
- script.queue_visible = false;
- return;
- }
-
- // display new queue
- if (!script.queue.isEmpty()) {
- queue.parentNode.removeAttribute("hidden", "");
-
- let autoplay = document.querySelector('ytd-compact-autoplay-renderer #contents');
- if (autoplay) { autoplay.setAttribute("hidden", "") }
-
- forEach(script.queue.get(), function(item, index) {
- try {
- loadQueueItem(item, index, queue);
- } catch (ex) {
- console.log("Failed to display queue item", ex);
- }
- });
- } else {
- queue.parentNode.setAttribute("hidden", "");
-
- let autoplay = document.querySelector('ytd-compact-autoplay-renderer #contents');
- if (autoplay) { autoplay.removeAttribute("hidden", ""); }
-
- // restore autoplay suggestion in video player
- if (script.debug) { console.log("SetAsNextVideo: Restore suggestion"); }
- QueueItem.fromDOM(getAutoplaySuggestion()).setAsNextVideo();
-
- script.queue_visible = false;
- }
- }
-
- function loadQueueItem(item, index, queueContents) {
- item.clearBadges();
- if (index === 0) {
- if (script.debug) { console.log("SetAsNextVideo: Load first queue item"); }
- item.setAsNextVideo();
- item.addBadge("Play Now", ["QUEUE_BUTTON", "QUEUE_PLAY_NOW"]);
- // item.addBadge("↓", ["QUEUE_BUTTON", "QUEUE_MOVE_DOWN"]);
- item.addBadge("Remove", ["QUEUE_BUTTON", "QUEUE_REMOVE"]);
- } else {
- item.addBadge("Play Next", ["QUEUE_BUTTON", "QUEUE_PLAY_NEXT"]);
- // item.addBadge("↑", ["QUEUE_BUTTON", "QUEUE_MOVE_UP"]);
- // item.addBadge("↓", ["QUEUE_BUTTON", "QUEUE_MOVE_DOWN"]);
- item.addBadge("Remove", ["QUEUE_BUTTON", "QUEUE_REMOVE"]);
- }
- window.Polymer.dom(queueContents).appendChild(item.toNode(["queue-item"]));
- }
-
- function hideQueue() {
- script.queue_visible = false;
-
- if (script.debug) { console.log("hiding queue"); }
-
- let queue = document.querySelector('#youtube-play-next-queue-renderer #contents');
- if (!queue) { return; }
-
- // clear current content
- queue.innerHTML = "";
- }
-
- // The "remove queue and all its videos" button
- function initRemoveQueueButton(anchor) {
- let html = "<div class=\"queue-button remove-queue\">Remove Queue</div>";
- anchor.innerHTML = html;
-
- if (!anchor.querySelector(".flex-whitebox")) {
- anchor.classList.add("flex-none");
- anchor.insertAdjacentHTML("afterend", "<div class=\"flex-whitebox\"></div>");
- }
-
- anchor.querySelector('.remove-queue').addEventListener("click", function handler(e) {
- e.preventDefault();
- script.queue.reset();
- this.parentNode.innerHTML = "Up next";
- });
- }
-
- // *** THUMB OVERLAYS *** //
-
- function addThumbOverlay(thumbOverlays) {
- // we don't use the toggled icon, that's why both have the same values.
- let overlay = {
- "thumbnailOverlayToggleButtonRenderer": {
- "ytQueue": true,
- "isToggled": false,
- "toggledIcon": {iconType: "ADD"},
- "toggledTooltip": "Queue",
- "toggledAccessibility": {
- "accessibilityData": {
- "label": "Queue"
- }
- },
- "untoggledIcon": {iconType: "ADD"},
- "untoggledTooltip": "Queue",
- "untoggledAccessibility": {
- "accessibilityData": {
- "label": "Queue"
- }
- }
- }
- };
-
- thumbOverlays.push(overlay);
- }
-
- function hasThumbOverlay(videoOverlays) {
- for(let i = 0; i < videoOverlays.length; i++) {
- if (videoOverlays[i].thumbnailOverlayToggleButtonRenderer && videoOverlays[i].thumbnailOverlayToggleButtonRenderer.ytQueue) {
- return true;
- }
- }
- return false;
- }
-
- function initThumbOverlay(videoRenderer) {
- let videoData = getVideoData(videoRenderer);
-
- if (videoData && videoData.thumbnailOverlays && !hasThumbOverlay(videoData.thumbnailOverlays) && !videoData.upcomingEventData) {
- addThumbOverlay(videoData.thumbnailOverlays);
- }
- }
-
- function initThumbOverlays() {
- 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');
- forEach(videoRenderers, function(videoRenderer) {
- initThumbOverlay(videoRenderer);
- });
- }
-
- function addThumbOverlayClickListeners() {
- let overlays = document.querySelectorAll('ytd-thumbnail-overlay-toggle-button-renderer > yt-icon');
-
- forEach(overlays, function(overlay) {
- overlay.removeEventListener("click", handleThumbOverlayClick);
-
- if (overlay.parentNode.getAttribute("aria-label") !== "Queue") {
- return;
- }
-
- overlay.addEventListener("click", handleThumbOverlayClick);
- });
- }
-
- function handleThumbOverlayClick(event) {
- event.stopPropagation(); event.preventDefault();
-
- let path = event.path || (event.composedPath && event.composedPath()) || event._composedPath;
- for(let i = 0; i < path.length; i++) {
- 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"];
- if (tagNames.includes(path[i].tagName)) {
- let newQueueItem = QueueItem.fromDOM(path[i]);
- if (!script.queue.contains(newQueueItem.id)) {
- script.queue.enqueue(newQueueItem);
- openToast("Video Added to Queue", event.target);
- } else {
- openToast("Video Already Queued", event.target);
- }
- break;
- }
- }
- }
-
- // *** BUTTONS *** //
-
- function initQueueButtons() {
- // initQueueButtonAction("queue-play-now", () => script.queue.playNow());
- initQueueButtonAction("queue-play-next", (pos) => script.queue.playNext(pos+1));
- initQueueButtonAction("queue-remove", (pos) => script.queue.remove(pos));
- }
-
- function initQueueButtonAction(className, btnAction) {
- let buttons = document.getElementsByClassName(className);
-
- forEach(buttons, function(button, index) {
- let pos = index;
- if (!button.classList.contains("button-listener")) {
- button.addEventListener("click", function(event) {
- event.preventDefault();
- event.stopPropagation();
- btnAction(pos);
- });
- button.classList.add("button-listener");
- }
- });
- }
-
- // *** POPUPS *** //
-
- function openToast(text, target) {
- let openPopupAction = {
- "openPopupAction": {
- "popup": {
- "notificationActionRenderer": {
- "responseText": {simpleText: text},
- "trackingParams": ""
- }
- },
- "popupType": "TOAST"
- }
- };
-
- let popupContainer = document.querySelector('ytd-popup-container');
- popupContainer.handleOpenPopupAction_(openPopupAction, target);
- }
-
- // *** LOCALSTORAGE *** //
-
- function getCache(key) {
- return JSON.parse(localStorage.getItem("YTQUEUE-MODERN#" + script.version + "#" + key));
- }
-
- function deleteCache(key) {
- localStorage.removeItem("YTQUEUE-MODERN#" + script.version + "#" + key);
- }
-
- function setCache(key, value) {
- localStorage.setItem("YTQUEUE-MODERN#" + script.version + "#" + key, JSON.stringify(value));
- }
-
- // *** CSS *** //
-
- // injecting css
- function injectCSS() {
- let css = `
- #youtube-play-next-queue-renderer {
- height: 310px;
- position: sticky; /* needed for chrome to show resize handler */
- border: 1px solid var(--yt-spec-10-percent-layer);
- padding: 5px 0 0 5px;
- margin-bottom: 16px;
- overflow-y: visible;
- overflow-x: hidden;
- resize: vertical;
- }
-
- ytd-compact-autoplay-renderer > #contents { padding-bottom: 8px }
-
- .queue-item { margin-top: 0px !important; margin-bottom: 6px !important; }
- .queue-item #metadata-line { display: none; }
-
- .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); }
- .queue-button.queue-play-now, .queue-button.queue-play-next { margin: 5px 3px 5px 0 !important; }
- .queue-button:hover { box-shadow: 0px 0px 3px black; }
- [dark] .queue-button:hover { box-shadow: 0px 0px 3px white; }
-
- ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] { bottom: 0; top: auto !important; right: auto; left: 0; }
- ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] #label-container { left: 28px !important; right: auto !important; }
- ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] #label-container > #label { padding: 0 8px 0 2px !important; }
- ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] paper-tooltip { right: -70px !important; left: auto !important }
- .queue-item ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queue] { display: none; }
-
- ytd-thumbnail-overlay-toggle-button-renderer[aria-label=Queued] { display: none; }
- `;
-
- let style = document.createElement("style");
- style.type = "text/css";
- if (style.styleSheet){
- style.styleSheet.cssText = css;
- } else {
- style.appendChild(document.createTextNode(css));
- }
-
- (document.body || document.head || document.documentElement).appendChild(style);
- }
-
- // *** FUNCTIONALITY *** //
-
- function forEach(array, callback, scope) {
- for (let i = 0; i < array.length; i++) {
- callback.call(scope, array[i], i);
- }
- }
-
- // When you want to remove elements
- function forEachReverse(array, callback, scope) {
- for (let i = array.length - 1; i >= 0; i--) {
- callback.call(scope, array[i], i);
- }
- }
-
- // hh:mm:ss => only seconds
- function hmsToSeconds(str) {
- let p = str.split(":"),
- s = 0, m = 1;
-
- while (p.length > 0) {
- s += m * parseInt(p.pop(), 10);
- m *= 60;
- }
-
- return s;
- }
- }
-
- function youtube_search_while_watching_video() {
- let script = {
- initialized: false,
-
- ytplayer: null,
-
- search_bar: null,
- search_timeout: null,
- search_suggestions: [],
- searched: false,
-
- debug: false
- };
-
- document.addEventListener("DOMContentLoaded", initScript);
-
- // reload script on page change using youtube polymer fire events
- window.addEventListener("yt-page-data-updated", function(event) {
- if (script.debug) { console.log("# page updated #"); }
- startScript(2);
- });
-
- function initScript() {
- if (script.debug) { console.log("### Youtube Search While Watching Video Initializing ###"); }
-
- initSearch();
- injectCSS();
-
- if (script.debug) { console.log("### Youtube Search While Watching Video Initialized ###"); }
- script.initialized = true;
-
- startScript(5);
- }
-
- function startScript(retry) {
- if (script.initialized && isPlayerAvailable()) {
- if (script.debug) { console.log("videoplayer is available"); }
- if (script.debug) { console.log("ytplayer: ", script.ytplayer); }
-
- if (script.ytplayer) {
- try {
- if (script.debug) { console.log("initializing search"); }
- loadSearch();
- } catch (error) {
- console.log("Failed to initialize search: ", (script.debug) ? error : error.message);
- }
- }
- } else if (retry > 0) { // fix conflict with Youtube+ script
- setTimeout( function() {
- startScript(--retry);
- }, 1000);
- } else {
- if (script.debug) { console.log("videoplayer is unavailable"); }
- }
- }
-
- // *** VIDEOPLAYER *** //
-
- function getVideoPlayer() {
- return document.getElementById('movie_player');
- }
-
- function isPlayerAvailable() {
- script.ytplayer = getVideoPlayer();
- return script.ytplayer !== null && script.ytplayer.getVideoData().video_id;
- }
-
- function isPlaylist() {
- return script.ytplayer.getVideoStats().list;
- }
-
- function isLivePlayer() {
- return script.ytplayer.getVideoData().isLive;
- }
-
- // *** SEARCH *** //
-
- function initSearch() {
- // callback function for search suggestion results
- window.suggestions_callback = suggestionsCallback;
- }
-
- function loadSearch() {
- // prevent double searchbar
- let playlistOrLiveSearchBar = document.querySelector('#suggestions-search.playlist-or-live');
- if (playlistOrLiveSearchBar) { playlistOrLiveSearchBar.remove(); }
-
- let searchbar = document.getElementById('suggestions-search');
- if (!searchbar) {
- createSearchBar();
- } else {
- searchbar.value = "";
- }
-
- script.searched = false;
- cleanupSuggestionRequests();
- }
-
- function createSearchBar() {
- let anchor, html;
-
- anchor = document.querySelector('ytd-compact-autoplay-renderer > #contents');
- if (anchor) {
- html = "<input id=\"suggestions-search\" type=\"search\" placeholder=\"Search\">";
- anchor.insertAdjacentHTML("afterend", html);
- } else { // playlist, live video or experimental youtube layout (where autoplay is not a separate renderer anymore)
- anchor = document.querySelector('#related > ytd-watch-next-secondary-results-renderer');
- if (anchor) {
- html = "<input id=\"suggestions-search\" class=\"playlist-or-live\" type=\"search\" placeholder=\"Search\">";
- anchor.insertAdjacentHTML("beforebegin", html);
- }
- }
-
- let searchBar = document.getElementById('suggestions-search');
- if (searchBar) {
- script.search_bar = searchBar;
-
- new window.autoComplete({
- selector: '#suggestions-search',
- minChars: 1,
- delay: 250,
- source: function(term, suggest) {
- suggest(script.search_suggestions);
- },
- onSelect: function(event, term, item) {
- prepareNewSearchRequest(term);
- }
- });
-
- script.search_bar.addEventListener("keyup", function(event) {
- if (this.value === "") {
- resetSuggestions();
- } else {
- searchSuggestions(this.value);
- }
- });
-
- // seperate keydown listener because the search listener blocks keyup..?
- script.search_bar.addEventListener("keydown", function(event) {
- const ENTER = 13;
- if (this.value.trim() !== "" && (event.key == "Enter" || event.keyCode === ENTER)) {
- prepareNewSearchRequest(this.value.trim());
- }
- });
-
- script.search_bar.addEventListener("search", function(event) {
- if(this.value === "") {
- script.search_bar.blur(); // close search suggestions dropdown
- script.search_suggestions = []; // clearing the search suggestions
-
- resetSuggestions();
- }
- });
-
- script.search_bar.addEventListener("focus", function(event) {
- this.select();
- });
- }
- }
-
- // callback from search suggestions attached to window
- function suggestionsCallback(data) {
- let raw = data[1]; // extract relevant data from json
- let suggestions = raw.map(function(array) {
- return array[0]; // change 2D array to 1D array with only suggestions
- });
- if (script.debug) { console.log(suggestions); }
- script.search_suggestions = suggestions;
- }
-
- function searchSuggestions(value) {
- if (script.search_timeout !== null) { clearTimeout(script.search_timeout); }
-
- // youtube search parameters
- const GeoLocation = window.yt.config_.INNERTUBE_CONTEXT_GL;
- const HostLanguage = window.yt.config_.INNERTUBE_CONTEXT_HL;
-
- // only allow 1 suggestion request every 100 milliseconds
- script.search_timeout = setTimeout(function() {
- if (script.debug) { console.log("suggestion request send", this.searchValue); }
- let scriptElement = document.createElement("script");
- scriptElement.type = "text/javascript";
- scriptElement.className = "suggestion-request";
- 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";
- (document.body || document.head || document.documentElement).appendChild(scriptElement);
- }.bind({searchValue:value}), 100);
- }
-
- function cleanupSuggestionRequests() {
- let requests = document.getElementsByClassName('suggestion-request');
- forEachReverse(requests, function(request) {
- request.remove();
- });
- }
-
- // send new search request (with the search bar)
- function prepareNewSearchRequest(value) {
- if (script.debug) { console.log("searching for " + value); }
-
- script.search_bar.blur(); // close search suggestions dropdown
- script.search_suggestions = []; // clearing the search suggestions
-
- sendSearchRequest("https://www.youtube.com/results?pbj=1&search_query=" + encodeURIComponent(value));
- }
-
- // given the url, retrieve the search results
- function sendSearchRequest(url) {
- let xmlHttp = new XMLHttpRequest();
- xmlHttp.onreadystatechange = function() {
- if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
- processSearch(xmlHttp.responseText);
- }
- };
-
- xmlHttp.open("GET", url, true);
- xmlHttp.setRequestHeader("x-youtube-client-name", window.yt.config_.INNERTUBE_CONTEXT_CLIENT_NAME);
- xmlHttp.setRequestHeader("x-youtube-client-version", window.yt.config_.INNERTUBE_CONTEXT_CLIENT_VERSION);
- xmlHttp.setRequestHeader("x-youtube-client-utc-offset", new Date().getTimezoneOffset() * -1);
-
- if (window.yt.config_.ID_TOKEN) { // null if not logged in
- xmlHttp.setRequestHeader("x-youtube-identity-token", window.yt.config_.ID_TOKEN);
- }
-
- xmlHttp.send(null);
- }
-
- // process search request
- function processSearch(responseText) {
- try {
- let data = JSON.parse(responseText);
-
- let found = searchJson(data, (key, value) => {
- if (key === "itemSectionRenderer") {
- if (script.debug) { console.log(value.contents); }
- let succeeded = createSuggestions(value.contents);
- return succeeded;
- }
- return false;
- });
-
- if (!found) {
- alert("The search request was succesful but the script was unable to parse the results");
- }
- } catch (error) {
- alert("Failed to retrieve search data, sorry!\nError message: " + error.message + "\nSearch response: " + responseText);
- }
- }
-
- function searchJson(json, func) {
- let found = false;
-
- for (let item in json) {
- found = func.apply(this, [item, json[item]]);
- if (found) { break; }
-
- if (json[item] !== null && typeof(json[item]) == "object") {
- found = searchJson(json[item], func);
- if (found) { break; }
- }
- }
-
- return found;
- }
-
- // *** HTML & CSS *** //
-
- function createSuggestions(data) {
- // filter out promotional stuff
- if (data.length < 10) {
- return false;
- }
-
- // remove current suggestions
- let hidden_continuation_item_renderer;
- 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');
- forEachReverse(watchRelated.children, function(item) {
- if (item.tagName === "YTD-CONTINUATION-ITEM-RENDERER") {
- item.setAttribute("hidden", "");
- hidden_continuation_item_renderer = item;
- } else if (item.tagName !== "YTD-COMPACT-AUTOPLAY-RENDERER") {
- item.remove();
- }
- });
-
- // create suggestions
- forEach(data, function(videoData) {
- if (videoData.videoRenderer || videoData.compactVideoRenderer) {
- window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.videoRenderer || videoData.compactVideoRenderer, "ytd-compact-video-renderer"));
- } else if (videoData.radioRenderer || videoData.compactRadioRenderer) {
- window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.radioRenderer || videoData.compactRadioRenderer, "ytd-compact-radio-renderer"));
- } else if (videoData.playlistRenderer || videoData.compactPlaylistRenderer) {
- window.Polymer.dom(watchRelated).appendChild(videoQueuePolymer(videoData.playlistRenderer || videoData.compactPlaylistRenderer, "ytd-compact-playlist-renderer"));
- }
- });
-
- if (hidden_continuation_item_renderer) {
- watchRelated.appendChild(hidden_continuation_item_renderer);
- }
-
- script.searched = true;
-
- return true;
- }
-
- function resetSuggestions() {
- if (script.searched) {
- 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");
- let data = itemSectionRenderer.__data.data;
- createSuggestions(data.contents || data.results);
-
- // restore continuation renderer
- let continuation = itemSectionRenderer.querySelector('ytd-continuation-item-renderer[hidden]');
- if (continuation) {
- continuation.removeAttribute("hidden");
- }
- }
-
- script.searched = false;
- }
-
- function videoQueuePolymer(videoData, type) {
- let node = document.createElement(type);
- node.classList.add("style-scope", "ytd-watch-next-secondary-results-renderer", "yt-search-generated");
- node.data = videoData;
- return node;
- }
-
- function injectCSS() {
- let css = `
- .autocomplete-suggestions {
- text-align: left; cursor: default; border: 1px solid var(--ytd-searchbox-legacy-border-color); border-top: 0; background: var(--ytd-searchbox-background);
- 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);
- }
- .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); }
- .autocomplete-suggestion b { font-weight: normal; color: #b31217; }
- .autocomplete-suggestion.selected { background: #ddd; }
- [dark] .autocomplete-suggestion.selected { background: #333; }
-
- ytd-compact-autoplay-renderer { padding-bottom: 0px; }
-
- #suggestions-search {
- outline: none; width: 100%; padding: 6px 5px; margin-bottom: 16px;
- border: 1px solid var(--ytd-searchbox-legacy-border-color); border-radius: 2px 0 0 2px;
- box-shadow: inset 0 1px 2px var(--ytd-searchbox-legacy-border-shadow-color);
- color: var(--ytd-searchbox-text-color); background-color: var(--ytd-searchbox-background);
- }
- `;
-
- let style = document.createElement("style");
- style.type = "text/css";
- if (style.styleSheet){
- style.styleSheet.cssText = css;
- } else {
- style.appendChild(document.createTextNode(css));
- }
-
- (document.body || document.head || document.documentElement).appendChild(style);
- }
-
- // *** FUNCTIONALITY *** //
-
- function forEach(array, callback, scope) {
- for (let i = 0; i < array.length; i++) {
- callback.call(scope, array[i], i);
- }
- }
-
- // When you want to remove elements
- function forEachReverse(array, callback, scope) {
- for (let i = array.length - 1; i >= 0; i--) {
- callback.call(scope, array[i], i);
- }
- }
- }
-
- // ================================================================================= //
- // =============================== INJECTING SCRIPTS =============================== //
- // ================================================================================= //
-
- document.documentElement.setAttribute("youtube-play-next-queue", "");
-
- if (!document.getElementById("autocomplete_script")) {
- let autoCompleteScript = document.createElement('script');
- autoCompleteScript.id = "autocomplete_script";
- autoCompleteScript.appendChild(document.createTextNode('window.autoComplete = ' + autoComplete + ';'));
- (document.body || document.head || document.documentElement).appendChild(autoCompleteScript);
- }
-
- if (!document.getElementById("play_next_queue_script")) {
- let playNextQueueScript = document.createElement('script');
- playNextQueueScript.id = "play_next_queue_script";
- playNextQueueScript.appendChild(document.createTextNode('('+ youtube_play_next_queue_modern +')();'));
- (document.body || document.head || document.documentElement).appendChild(playNextQueueScript);
- }
-
- if (!document.getElementById("search_while_watching_video")) {
- let searchWhileWatchingVideoScript = document.createElement('script');
- searchWhileWatchingVideoScript.id = "search_while_watching_video";
- searchWhileWatchingVideoScript.appendChild(document.createTextNode('('+ youtube_search_while_watching_video +')();'));
- (document.body || document.head || document.documentElement).appendChild(searchWhileWatchingVideoScript);
- }
- })();