// ==UserScript==
// @name Youtube Play Next Queue
// @version 2.2.1
// @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 (MODERN) ======================= //
// ================================================================================ //
function youtube_play_next_queue_modern() {
let script = {
version: "2.0.0",
initialized: false,
queue: null,
ytplayer: null,
autoplay_suggestion: null,
queue_rendered_observer: null,
video_renderer_observer: null,
playnext_data_observer: null,
debug: false
};
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 (material) #"); }
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("### Modern youtube loaded ###"); }
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 && !isPlaylist()) {
if (script.debug) { console.log("initializing queue"); }
loadQueue();
if (script.debug) { console.log("initializing video statelistener"); }
initVideoStateListener();
if (script.debug) { console.log("initializing playnext data observer"); }
initPlayNextDataObserver();
}
} 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);
// run handler once to make sure queue is in sync
handleVideoStateChanged(script.ytplayer.getPlayerState());
} else {
if (script.debug) { console.log("statelistener already initialized"); }
}
}
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();
}
}
if ((videoState === PLAYING_STATE || videoState === PAUSED_STATE) && !script.queue.isEmpty()) {
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.getElementById('confirm-button');
if (button && button.offsetParent === null) {
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('ytd-compact-autoplay-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() && !isPlaylist() && !isLivePlayer()) {
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) {
nextQueueItem.setAsNextVideo();
}
}
});
}
});
let observable = document.querySelector('.ytp-next-button');
script.playnext_data_observer.observe(observable, { attributes: true });
}
/* function initVideoRendererObserver() {
if (script.video_renderer_observer) {
script.video_renderer_observer.disconnect();
}
script.video_renderer_observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
forEach(mutation.addedNodes, function(node) {
let tagNames = ["YTD-COMPACT-VIDEO-RENDERER", "YTD-GRID-VIDEO-RENDERER", "YTD-VIDEO-RENDERER"];
if (tagNames.includes(node.tagName)) {
initThumbOverlay(node);
// If youtube updates node data, reinit thumb overlay
new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
initThumbOverlay(mutation.target);
});
}).observe(node, { attributes: true });
}
});
});
});
let observable = document.querySelector('ytd-watch-next-secondary-results-renderer > #items');
script.video_renderer_observer.observe(observable, { childList: 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;
}
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 getVideoData(element) {
var data = element.__data.data;
if (data.content) {
return data.content.videoRenderer;
} else {
return data;
}
}
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;
}
getRelatedVideoArgs() {
let args = {
id: this.data.videoId,
title: this.data.title.simpleText || this.data.title.runs[0].text,
author: this.data.author || this.data.shortBylineText.runs[0].text,
length_seconds: hmsToSeconds(this.getVideoLength()),
aria_label: this.data.title.accessibility.accessibilityData.label,
iurlmq: this.getSmallestThumb().url,
iurlhq: this.getBiggestThumb().url,
session_data: "itct=" + this.data.navigationEndpoint.clickTrackingParams,
short_view_count_text: this.data.shortViewCountText ? this.data.shortViewCountText.simpleText : "",
endscreen_autoplay_session_data: "autonav=1&playnext=1&itct=" + this.data.navigationEndpoint.clickTrackingParams,
};
return args;
}
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;
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;
// if (watchNextData.webWatchNextResponseExtensionData) {
// let relatedVideoConfig = watchNextData.webWatchNextResponseExtensionData;
// let relatedVideosArgsList = relatedVideoConfig.relatedVideoArgs.split(",");
// let firstVideoArgs = relatedVideosArgsList[0];
// let otherVideoArgs = relatedVideosArgsList.slice(1).join(",");
// let videoParams = this.getRelatedVideoArgs();
// // changing next video with first from queue
// forEach(Object.keys(videoParams), function(param) {
// let re = new RegExp("(" + param + ")=(.[^&]+)", "g");
// firstVideoArgs = firstVideoArgs.replace(re, function($0, param, value) {
// return param + "=" + encodeURIComponent(videoParams[param] || "");
// });
// });
// script.ytplayer.updateVideoData(JSON.parse('{"rvs":"' + firstVideoArgs + ',' + otherVideoArgs + '"}'));
// } else {
let watchNextResponse = { "raw_watch_next_response" : watchNextData};
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);
// }
}
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.setAttribute("draggable", true);
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) {
cachedQueue = cachedQueue.map(queueItem => QueueItem.fromJSON(queueItem));
script.queue.set(cachedQueue);
} else {
setCache("QUEUE", script.queue.get());
}
}
function loadQueue() {
// prepare html for queue
let queue = document.querySelector('ytd-compact-autoplay-renderer');
if (!queue) {
return;
}
let suggestion = queue.querySelector('ytd-compact-video-renderer');
if (suggestion) {
script.autoplay_suggestion = QueueItem.fromDOM(suggestion);
}
// show the queue if not empty
if (!script.queue.isEmpty()) {
displayQueue();
}
}
function displayQueue() {
if (script.debug) { console.log("showing queue: ", script.queue.get()); }
let queue = document.querySelector('ytd-compact-autoplay-renderer');
if (!queue) { return; }
let queueContents = queue.querySelector('#contents');
if (!queueContents) { return; }
initQueueRenderedObserver();
// clear current content
queueContents.innerHTML = "";
// display new queue
if (!script.queue.isEmpty()) {
forEach(script.queue.get(), function(item, index) {
try {
loadQueueItem(item, index, queueContents);
} catch (ex) {
console.log("Failed to display queue item", ex);
}
});
// show autoplay suggestion under queue if it is not queued
if (!script.queue.contains(script.autoplay_suggestion.id)) {
window.Polymer.dom(queueContents).appendChild(script.autoplay_suggestion.toNode());
}
// initialize remove queue button.
let upNext = queue.querySelector("#upnext");
if (upNext) {
initRemoveQueueButton(upNext);
}
} else {
// restore autoplay suggestion (queue is empty)
script.autoplay_suggestion.setAsNextVideo();
window.Polymer.dom(queueContents).appendChild(script.autoplay_suggestion.toNode());
// restore up next header
let upNext = queue.querySelector("#upnext");
if (upNext) {
upNext.innerHTML = "Up next";
}
}
}
function loadQueueItem(item, index, queueContents) {
item.clearBadges();
if (index === 0) {
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"]));
}
// 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 = `
.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; }
.queue-item #metadata-line { display: none; }
#upnext.flex-none { flex: 0 !important; white-space: nowrap; }
#upnext > .queue-button { font-size: 1.4rem; font-weight: 500; margin: 0 !important; }
.flex-whitebox { flex: 1; }
[draggable] {
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
user-select: none;
/* Required to make elements draggable in old WebKit */
-khtml-user-drag: element;
-webkit-user-drag: element;
}
`;
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() {
var 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();
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
var playlistOrLiveSearchBar = document.querySelector('#suggestions-search.playlist-or-live');
if (playlistOrLiveSearchBar) { playlistOrLiveSearchBar.remove(); }
var searchbar = document.getElementById('suggestions-search')
if (!searchbar) {
createSearchBar();
} else {
searchbar.value = "";
}
script.searched = false;
cleanupSuggestionRequests();
}
function createSearchBar() {
var 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 or live video?
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);
}
}
var 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) {
var raw = data[1]; // extract relevant data from json
var 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); }
var 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() {
var 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) {
var 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) {
var data = JSON.parse(responseText);
if (data && data[1] && data[1].response) {
try {
// dat chain o.O
var videosData = data[1].response.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
if (script.debug) { console.log(videosData); }
createSuggestions(videosData);
script.searched = true;
} catch (error) {
alert("Failed to retrieve search data, sorry! " + error.message);
}
}
}
// *** HTML & CSS *** //
function createSuggestions(data) {
// remove current suggestions
var 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-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"));
}
});
}
function resetSuggestions() {
if (script.searched) {
var itemSectionRenderer = document.querySelector('#related ytd-watch-next-secondary-results-renderer #items ytd-item-section-renderer') || document.querySelector("#related ytd-watch-next-secondary-results-renderer");
var data = itemSectionRenderer.__data.data;
createSuggestions(data.contents || data.results);
}
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() {
var css = `
.autocomplete-suggestions {
text-align: left; cursor: default; border: 1px solid var(--ytd-searchbox-legacy-border-color); border-top: 0; background: var(--yt-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(--yt-placeholder-text); }
.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: 8px 0 0 0;
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(--yt-searchbox-text-color); background-color: var(--yt-searchbox-background);
}
#suggestions-search.playlist-or-live { margin-bottom: 16px; }
`;
var 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 (var i = 0; i < array.length; i++) {
callback.call(scope, array[i], i);
}
}
// When you want to remove elements
function forEachReverse(array, callback, scope) {
for (var i = array.length - 1; i >= 0; i--) {
callback.call(scope, array[i], i);
}
}
}
// ================================================================================= //
// =============================== INJECTING SCRIPTS =============================== //
// ================================================================================= //
var autoCompleteScript = document.createElement('script');
autoCompleteScript.appendChild(document.createTextNode('window.autoComplete = ' + autoComplete + ';'));
(document.body || document.head || document.documentElement).appendChild(autoCompleteScript);
var queueScriptModern = document.createElement('script');
queueScriptModern.appendChild(document.createTextNode('('+ youtube_play_next_queue_modern +')();'));
(document.body || document.head || document.documentElement).appendChild(queueScriptModern);
var searchScript = document.createElement('script');
searchScript.appendChild(document.createTextNode('('+ youtube_search_while_watching_video +')();'));
(document.body || document.head || document.documentElement).appendChild(searchScript);
})();