// ==UserScript==
// @name Youtube Play Next Queue
// @version 1.1.3
// @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
// @include https://www.youtube.com*
// @license GPL version 2 or any later version; 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
// @grant none
// @noframes
// ==/UserScript==
(function() {
var script = {
ytplayer: null,
playnext: true,
queue: new Queue(),
version: '1.1.2',
search_timeout: null,
search_suggestions: [],
debug: false
};
// callback function for search results
window.search_callback = search_callback;
// youtube search parameters
const GeoLocation = window.yt.config_.INNERTUBE_CONTEXT_GL;
const HostLanguage = window.yt.config_.INNERTUBE_CONTEXT_HL;
// reload script on page change using youtube spf events (http://youtube.github.io/js/documentation/events/)
window.addEventListener("spfdone", function(e) {
if (script.debug) console.log("new page loaded");
clearSearchRequests();
if (isPlayerAvailable()) {
startScript(2);
}
});
main();
function main() {
initGlobalScrollListener();
addCSS();
if (isPlayerAvailable()) {
if (script.debug) console.log("player available");
startScript(5);
} else {
if (script.debug) console.log("player unavailable");
}
}
function startScript(retry) {
script.ytplayer = getVideoPlayer();
if (script.debug) console.log(script.ytplayer);
if (script.ytplayer) {
if (getVideoInfoFromUrl(document.location.href, "t") == "0s")
script.ytplayer.seekTo(0);
if (script.debug) console.log("initialising queue");
initQueue();
if (script.debug) console.log("initialising search");
initSearch();
if (script.debug) console.log("initialising video state listener");
initVideoStateListener();
if (script.debug) console.log("initialising queue buttons");
initQueueButtons();
} else if (retry > 0) { // fix conflict with Youtube+ script
setTimeout( function() {
startScript(retry--);
}.bind(retry), 1000);
}
}
// *** LISTENERS *** //
function initVideoStateListener() {
// play next video in queue if current video is finished playing (state equal to 0)
script.ytplayer.addEventListener("onStateChange", function(videoState) {
if (script.debug) console.log("state changed", videoState);
const FINISHED_STATE = 0;
if (videoState === FINISHED_STATE && script.playnext === true && !script.queue.isEmpty()) {
script.playnext = false;
var next = script.queue.dequeue();
playNextVideo(next.id);
} else if (videoState !== FINISHED_STATE) {
script.playnext = true;
}
});
}
// Did new content load? Triggered everytime you scroll
function initGlobalScrollListener() {
document.addEventListener("scroll", function scroll(event) {
try {
if (isPlayerAvailable()) {
if (script.ytplayer === null) {
script.ytplayer = getVideoPlayer();
startScript(0);
} else {
initQueueButtons();
}
}
} catch(error) {
console.error("Couldn't initialize add to queue buttons \n" + error.message);
}
event.currentTarget.removeEventListener(event.type, scroll);
if (script.debug) console.log("scroll");
setTimeout( function() {
initGlobalScrollListener();
}, 1000);
});
}
// *** OBJECTS *** //
// video object
function ytVideo(name, id, html, anchor) {
this.name = name;
this.id = id;
this.html = html;
this.buttonAnchor = anchor;
}
// extended video object
function extendedYtVideo(name, id, html, anchor, channelHTML, time, stats, thumb) {
this.name = name;
this.id = id;
this.html = html;
this.channelHTML = channelHTML;
this.time = time;
this.stats = stats;
this.buttonAnchor = anchor;
this.thumb = thumb;
}
// Queue object
function Queue() {
var queue = [];
this.get = function() {
return queue;
};
this.set = function(newQueue) {
queue = newQueue;
setCache("queue", this.get());
};
this.isEmpty = function() {
return 0 === queue.length;
};
this.reset = function() {
queue = [];
this.update(0);
};
this.enqueue = function(item) {
queue.push(item);
this.update(500);
};
this.dequeue = function() {
var item = queue.shift();
this.update(0);
return item;
};
this.remove = function(index) {
queue.splice(index, 1);
this.update(250);
};
this.playNext = function(index) {
var video = queue.splice(index, 1);
queue.unshift(video[0]);
this.update(0);
};
this.playNow = function(index) {
var video = queue.splice(index, 1);
this.update(0);
playNextVideo(video[0].id);
};
this.showQueue = function() {
var html = "";
queue.forEach( function(item) {
html += item.html;
});
return html;
};
this.update = function(delay) {
setCache("queue", this.get());
if (script.debug) console.log(this.get().slice());
setTimeout(function() {displayQueue();}, delay);
};
}
// *** VIDEO & PLAYER *** //
// play next video behavior depending on if you're watching fullscreen
function playNextVideo(nextVideoId) {
if (script.debug) console.log("playing next song");
if (isPlayerFullscreen()) {
script.ytplayer.loadVideoById(nextVideoId, 0);
} else {
window.spf.navigate("https://www.youtube.com/watch?v=" + nextVideoId + "&t=0s");
}
}
function getVideoPlayer() {
return document.getElementById("movie_player");
}
function isPlayerAvailable() {
return /https:\/\/www\.youtube\.com\/watch\?v=.*/.test(document.location.href) && !getVideoInfoFromUrl(document.location.href, "list") && document.getElementById("live-chat-iframe") === null;
}
function isPlayerFullscreen() {
return (script.ytplayer.classList.contains("ytp-fullscreen"));
}
function getVideoInfoFromUrl(url, info) {
if (url.indexOf('?') === -1)
return null;
var urlVariables = url.split('?')[1].split('&'),
varName;
for (var i = 0; i < urlVariables.length; i++) {
varName = urlVariables[i].split('=');
if (varName[0] === info) {
return varName[1] === undefined ? null : varName[1];
}
}
}
// extracting video information and creating a video object (that can be added to the queue)
function findVideoInformation(video, selector) {
var anchor = video.querySelector(selector + " .yt-uix-sessionlink:not(.related-playlist)");
if (anchor) {
var videoTitle = video.querySelector("span.title").textContent.trim();
var id = getVideoInfoFromUrl(video.querySelector("a.yt-uix-sessionlink").href, "v");
var newVidObject = new ytVideo(videoTitle, id, video.outerHTML, anchor);
return newVidObject;
}
return null;
}
// *** QUEUE *** //
function initQueue() {
var cachedQueue = getCache('queue');
if (cachedQueue) {
script.queue.set(cachedQueue);
} else {
setCache('queue', script.queue.get());
}
// prepare html for queue
var queue = document.getElementsByClassName("autoplay-bar")[0];
queue.classList.add("video-list");
queue.id = "watch-queue";
queue.setAttribute("style", "list-style:none");
// add class to suggestion video so it doesn't get queue related buttons
var suggestion = queue.getElementsByClassName("related-list-item")[0];
suggestion.classList.add("suggestion");
// show the queue if not empty
if (!script.queue.isEmpty()) {
if (script.debug) console.log("showing queue");
if (script.debug) console.log(script.queue.get());
displayQueue();
}
}
function displayQueue() {
var html = script.queue.showQueue();
var queue = document.querySelector(".autoplay-bar");
var anchor = document.querySelector(".watch-sidebar-head");
// cleanup current queue
var li = document.querySelectorAll(".autoplay-bar > li.video-list-item");
if (li) {
for (var i = li.length - 1; i >= 0; i--) {
li[i].remove();
}
}
// display new queue
if (html !== null) {
anchor.insertAdjacentHTML("afterend", html);
// add remove buttons
var items = queue.querySelectorAll(".related-list-item:not(.suggestion)");
for (var z = 0; z < items.length; z++) {
var video = findVideoInformation(items[z], "#watch-queue");
// remove addbutton if there is one
var addedButton = items[z].querySelector(".queue-add");
if (addedButton)
addedButton.parentNode.remove();
if (video) {
if (z > 0) {
playNextButton(video, z);
} else {
playNowButton(video, z);
}
removeButton(video, z);
}
}
// replace autoplay options with remove queue button
var autoplay = queue.querySelector(".checkbox-on-off");
if (autoplay && !script.queue.isEmpty()) {
removeQueueButton(autoplay);
}
// add queue button to suggestion video
var suggestion = queue.querySelector(".suggestion:not(.processed)");
if (suggestion && !script.queue.isEmpty()) {
var suggestionVideo = findVideoInformation(suggestion, "#watch-queue");
suggestion.classList.add("processed");
suggestionAddButton(suggestionVideo, suggestion);
}
// triggering lazyload
window.scrollTo(window.scrollX, window.scrollY + 1);
window.scrollTo(window.scrollX, window.scrollY - 1);
}
// remove not interested menu
var menu = queue.getElementsByClassName("yt-uix-menu-trigger");
for (var j = menu.length - 1; j >= 0; j--) {
menu[j].remove();
}
}
// *** BUTTONS *** //
// finding video's and adding the queue buttons
function initQueueButtons() {
var videos = document.querySelectorAll(".related-list-item:not(.processed-buttons)");
for (var j = 0; j < videos.length; j++) {
try {
var video = findVideoInformation(videos[j], "#watch-related");
videos[j].classList.add("processed-buttons");
if (video) {
addButton(video);
}
} catch(error) {
console.error("Couldn't initialize \"Add to queue\" button on a video \n" + error.message);
}
}
}
// The "add to queue" button
function addButton(video) {
var anchor = video.buttonAnchor;
var html = '<div class="queue-button yt-uix-button yt-uix-button-default yt-uix-button-size-default"><button class="yt-uix-button-content queue-add">Add to queue</button></div>';
anchor.insertAdjacentHTML('beforeend', html);
anchor.getElementsByClassName("queue-add")[0].addEventListener("click", function handler(e) {
e.preventDefault();
this.textContent = "Added!";
script.queue.enqueue(video);
e.currentTarget.removeEventListener(e.type, handler);
this.addEventListener("click", function (e) {
e.preventDefault();
});
});
}
// The "add to queue" button for the suggestion video
function suggestionAddButton(video, suggestion) {
var anchor = video.buttonAnchor;
var html = '<div class="queue-button yt-uix-button yt-uix-button-default yt-uix-button-size-default"><button class="yt-uix-button-content queue-add">Add to queue</button></div>';
anchor.insertAdjacentHTML('beforeend', html);
anchor.getElementsByClassName("queue-add")[0].addEventListener("click", function handler(e) {
e.preventDefault();
this.textContent = "Added!";
suggestion.classList.remove("suggestion");
video.html = suggestion.outerHTML;
script.queue.enqueue(video);
e.currentTarget.removeEventListener(e.type, handler);
suggestion.parentNode.removeChild(suggestion);
});
}
// The "remove from queue" button
function removeButton(video, nb) {
var anchor = video.buttonAnchor;
var html = '<div class="queue-button yt-uix-button yt-uix-button-default yt-uix-button-size-default" style="margin-left:3px"><button class="yt-uix-button-content queue-remove">Remove</button></div>';
anchor.insertAdjacentHTML("beforeend", html);
anchor.getElementsByClassName("queue-remove")[0].addEventListener('click', function handler(e) {
e.preventDefault();
this.textContent = "Removed!";
script.queue.remove(nb);
restoreAddButton(video.id);
e.currentTarget.removeEventListener(e.type, handler);
this.addEventListener("click", function (e) {
e.preventDefault();
});
});
}
// The "play next" button
function playNextButton(video, nb) {
var anchor = video.buttonAnchor;
var html = '<div class="queue-button yt-uix-button yt-uix-button-default yt-uix-button-size-default"><button class="yt-uix-button-content queue-next">Play Next</button></div>';
anchor.insertAdjacentHTML("beforeend", html);
anchor.getElementsByClassName("queue-next")[0].addEventListener('click', function handler(e) {
e.preventDefault();
this.textContent = "To the top!";
script.queue.playNext(nb);
e.currentTarget.removeEventListener(e.type, handler);
this.addEventListener("click", function (e) {
e.preventDefault();
});
});
}
// The "play now" button
function playNowButton(video, nb) {
var anchor = video.buttonAnchor;
var html = '<div class="queue-button yt-uix-button yt-uix-button-default yt-uix-button-size-default"><button class="yt-uix-button-content queue-now">Play Now</button></div>';
anchor.insertAdjacentHTML("beforeend", html);
anchor.getElementsByClassName("queue-now")[0].addEventListener("click", function handler(e) {
e.preventDefault();
this.textContent = "Playing!";
script.queue.playNow(nb);
e.currentTarget.removeEventListener(e.type, handler);
this.addEventListener("click", function (e) {
e.preventDefault();
});
});
}
// The "remove queue and all its videos" button
function removeQueueButton(anchor) {
var html = '<div class="queue-button yt-uix-button yt-uix-button-default yt-uix-button-size-default" style="margin:0px"><button class="yt-uix-button-content remove-queue">Remove Queue</button></div>';
anchor.innerHTML = html;
anchor.getElementsByClassName("remove-queue")[0].addEventListener("click", function handler(e) {
e.preventDefault();
this.textContent = "Removed!";
script.queue.reset();
restoreAddButton("*"); // restore all
e.currentTarget.removeEventListener(e.type, handler);
this.addEventListener("click", function (e) {
e.preventDefault();
});
});
}
function restoreAddButton(id) {
var videos = document.querySelectorAll(".related-list-item");
for (var j = 0; j < videos.length; j++) {
if (id === "*" || id === getVideoInfoFromUrl(videos[j].querySelector("a.yt-uix-sessionlink").href, "v")) {
// remove current addbutton if there is one
var addedButton = videos[j].querySelector(".queue-add");
if (addedButton)
addedButton.parentNode.remove();
// make new addbutton
var video = findVideoInformation(videos[j], "#watch-related");
if (video) {
addButton(video);
}
}
}
}
// *** SEARCH *** //
// initialize search
function initSearch() {
var anchor = document.querySelector("#watch7-sidebar-modules > div:nth-child(2)");
var html = '<input id="masthead-queueSearch" class="search-term yt-uix-form-input-bidi" type="text" placeholder="Search" style="outline: none; width:95%; padding: 5px 5px; margin: 0 4px">';
anchor.insertAdjacentHTML('afterbegin', html);
var input = document.getElementById("masthead-queueSearch");
// suggestion dropdown init
new autoComplete({
selector: '#masthead-queueSearch',
minChars: 1,
delay: 250,
source: function(term, suggest) {
suggest(script.search_suggestions);
},
onSelect: function(event, term, item) {
sendSearchRequest(term);
}
});
input.addEventListener('keydown', function(event) {
if (script.debug) console.log(e);
const ENTER = 13;
const BACKSPACE = 8;
if (this.value !== "" && event.keyCode === ENTER) {
sendSearchRequest(this.value);
} else if (this.value !== "" && event.keyCode === BACKSPACE) {
searchSuggestions(this.value);
} else {
searchSuggestions(this.value + event.key);
}
});
input.addEventListener('click', function(event) {
this.select();
});
}
// callback from search suggestions attached to window
function search_callback(data) {
var raw = data[1]; // extract relevant data from json
script.search_suggestions = raw.map(function(array) {
return array[0]; // change 2D array to 1D array with only suggestions
});
if (script.debug) console.log(script.search_suggestions);
}
// get search suggestions
function searchSuggestions(value) {
if (script.search_timeout !== null) clearTimeout(script.search_timeout);
// only allow 1 search request every 100 milliseconds
script.search_timeout = setTimeout( function() {
if (script.debug) console.log("search request send");
var scriptElement = document.createElement('script');
scriptElement.type = 'text/javascript';
scriptElement.className = 'search-request';
scriptElement.src = 'https://clients1.google.com/complete/search?client=youtube&hl=' + HostLanguage + '&gl=' + GeoLocation + '&gs_ri=youtube&ds=yt&q=' + encodeURIComponent(value) + '&callback=search_callback';
document.head.appendChild(scriptElement);
}.bind(value), 100);
}
// send search request
function sendSearchRequest(value) {
if (script.debug) console.log("searching for " + value);
document.getElementById("masthead-queueSearch").blur(); // close search suggestions dropdown
var nextPage = document.getElementById("watch-more-related-button");
if (nextPage !== null) nextPage.parentNode.removeChild(nextPage); // removing the "More Suggestions" link
script.search_suggestions = []; // clearing the search suggestions
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
var container = document.implementation.createHTMLDocument().documentElement;
container.innerHTML = xmlHttp.responseText;
processSearch(container);
}
};
xmlHttp.open('GET', 'https://www.youtube.com/results?q=' + encodeURIComponent(value), true); // true for asynchronous
xmlHttp.send(null);
}
function clearSearchRequests() {
var requests = document.getElementsByClassName("search-request");
if (requests) {
for (var i = requests.length - 1; i >= 0; i--) {
requests[i].remove();
}
}
}
// process search request
function processSearch(value) {
var videoList = value.getElementsByClassName("item-section")[0];
// remove current videos (and replace with searched videos later)
var ul = document.getElementById("watch-related");
var li = ul.querySelectorAll("li.video-list-item");
if (li) {
for (var i = li.length - 1; i >= 0; i--) {
li[i].remove();
}
}
// insert searched videos
var videos = videoList.querySelectorAll('.yt-lockup-video');
for (var j = videos.length - 1; j >= 0; j--) {
var video = videos[j];
try {
var videoId = video.dataset.contextItemId;
var videoTitle = video.querySelector('.yt-lockup-title > a').title;
var videoStats = video.querySelector('.yt-lockup-meta').innerHTML;
var videoTime = video.querySelector('.video-time').textContent;
var videoChannelHTML = video.querySelector('.yt-lockup-byline');
var videoThumb = video.querySelector('div.yt-lockup-thumbnail > a > div > span > img');
if (videoThumb && videoThumb.hasAttribute("data-thumb")) {
videoThumb = videoThumb.dataset.thumb;
} else if (videoThumb) {
videoThumb = videoThumb.src;
}
if (videoChannelHTML) {
videoChannelHTML = videoChannelHTML.textContent;
} else if (video.querySelector('.yt-lockup-description')) {
videoChannelHTML = "<a href=\"" + window.location.href + "\" class=\"spf-link\">" + video.querySelector('.yt-lockup-description').firstChild.textContent + "</a>";
}
var videoObject = new extendedYtVideo(videoTitle, videoId, null, null, videoChannelHTML, videoTime, videoStats, videoThumb);
if (script.debug) console.log(videoObject);
ul.insertAdjacentHTML("afterbegin", videoQueueHTML(videoObject).html);
} catch (error) {
console.error("failed to process video", video);
}
}
initQueueButtons();
}
// *** LOCALSTORAGE *** //
function getCache(key) {
return JSON.parse(localStorage.getItem("YTQUEUE#" + script.version + '#' + key));
}
function deleteCache(key) {
localStorage.removeItem("YTQUEUE#" + script.version + '#' + key);
}
function setCache(key, value) {
localStorage.setItem("YTQUEUE#" + script.version + '#' + key, JSON.stringify(value));
}
// *** HTML & CSS *** //
function videoQueueHTML(video) {
var strVar="";
strVar += "<li class=\"video-list-item related-list-item show-video-time related-list-item-compact-video\">";
strVar += " <div class=\"related-item-dismissable\">";
strVar += " <div class=\"content-wrapper\">";
strVar += " <a href=\"\/watch?v=" + video.id + "\" class=\"yt-uix-sessionlink content-link spf-link spf-link\" rel=\"spf-prefetch\" title=\"" + video.name + "\">";
strVar += " <span dir=\"ltr\" class=\"title\">" + video.name + "<\/span>";
strVar += " <span class=\"stat\">" + video.channelHTML + "<\/span>";
strVar += " <div class=\"yt-lockup-meta stat\">" + video.stats + "<\/div>";
strVar += " <\/a>";
strVar += " <\/div>";
strVar += " <div class=\"thumb-wrapper\">";
strVar += " <a href=\"\/watch?v=" + video.id + "\" class=\"yt-uix-sessionlink thumb-link spf-link spf-link\" rel=\"spf-prefetch\" tabindex=\"-1\" aria-hidden=\"true\">";
strVar += " <span class=\"yt-uix-simple-thumb-wrap yt-uix-simple-thumb-related\" tabindex=\"0\" data-vid=\"" + video.id + "\"><img aria-hidden=\"true\" style=\"top: 0px\" width=\"168\" height=\"94\" alt=\"\" src=\"" + video.thumb + "\"><\/span>";
strVar += " <\/a>";
strVar += " <span class=\"video-time\">"+ video.time +"<\/span>";
strVar += " <button class=\"yt-uix-button yt-uix-button-size-small yt-uix-button-default yt-uix-button-empty yt-uix-button-has-icon no-icon-markup addto-button video-actions spf-nolink hide-until-delayloaded addto-watch-later-button yt-uix-tooltip\" type=\"button\" onclick=\";return false;\" title=\"Watch Later\" role=\"button\" data-video-ids=\"" + video.id + "\" data-tooltip-text=\"Watch Later\"><\/button>";
strVar += " <\/div>";
strVar += " <\/div>";
strVar += "<\/li>";
video.html = strVar;
return video;
}
function addCSS() {
var css = `
.autocomplete-suggestions {
text-align: left; cursor: default; border: 1px solid #ccc; border-top: 0; background: #fff; box-shadow: -1px 1px 3px rgba(0,0,0,.1);
position: absolute; display: none; z-index: 9999; max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box;
}
.autocomplete-suggestion { position: relative; padding: 0 .6em; line-height: 23px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1.02em; color: #333; }
.autocomplete-suggestion b { font-weight: normal; color: #b31217; }
.autocomplete-suggestion.selected { background: #f0f0f0; }
#watch-related .yt-uix-button-size-default { display: none; }
#watch-related .processed-buttons:hover .yt-uix-button-size-default { display: inline-block; }
.queue-button { height: 15px; padding: 0.2em 0.4em 0.2em 0.4em; margin: 2px 0; }
.related-list-item span.title { max-height: 2.4em; }
`;
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet){
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
document.documentElement.appendChild(style);
}
})();