Youtube Play Next Queue

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

Mint 2017.04.07.. Lásd a legutóbbi verzió

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Youtube Play Next Queue
// @version      1.0.2
// @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://greasyfork.org/users/16080
// @grant        none
// @noframes
// ==/UserScript==

(function() {
	var script = {
		ytplayer: getVideoPlayer(),
		playnext: true,
		queue: new Queue(),
		version: '1.0.0',
		search_timeout: null,
		suggestions: [],
		debug: false
	};

	// callback function for search results
	window.search_callback = search_callback;

	// 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");
		script.ytplayer = getVideoPlayer();
		if (script.debug) console.log(script.ytplayer);
		if (isPlayerAvailable()) {
			startScript();
		}
	});

	addAutoCompleteCSS();

	function addAutoCompleteCSS() {
		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; }
            `;

		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);
	}

	//deleteCache('queue'); // for testing purposes
	if (isPlayerAvailable()) {
		startScript();
	}

	function startScript() {
		if (script.ytplayer) {
			if (getVideoInfoFromUrl(document.location.href, "t") == "0s")
				script.ytplayer.seekTo(0);
			startQueue();
		}

		try {
			globalScrollListener();
			findVideoThumbs();
		} catch(error) {
			console.error("Couldn't initialize add to queue buttons \n" + error.message);
		}
	}

	function startQueue() {
		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");
		initStateListener();
		if (!script.queue.isEmpty()) {
			if (script.debug) console.log("showing queue");
			if (script.debug) console.log(script.queue.get());
			displayQueue();
		}
	}

	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");
	}

	function initStateListener() {
		// play next video in queue if current video is finished playing (state equal to 0)
		script.ytplayer.addEventListener("onStateChange", function(e) {
			if (script.debug) console.log("state changed", e);
			if (e === 0 && script.playnext && !script.queue.isEmpty()) {
				script.playnext = false;
				var next = script.queue.dequeue();
				playNextVideo(next.id);
			} else if (e !== 0) {
				script.playnext = true;
			}
		});
	}

	// Did new content load? Triggered everytime you scroll
	function globalScrollListener() {
		document.addEventListener("scroll", function scroll(e) {
			try {
				if (isPlayerAvailable()) {
					findVideoThumbs();
				}
			} catch(error) {
				console.error("Couldn't initialize add to queue buttons \n" + error.message);
			}

			e.currentTarget.removeEventListener(e.type, scroll);
			if (script.debug) console.log("scroll");

			setTimeout( function() {
				globalScrollListener();
			}, 1000);
		});
	}

	// *** 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.suggestions);
			},
			onSelect: function(event, term, item) {
				sendSearchRequest(term);
			}
		});

		input.addEventListener('keydown', function(e) {
			if (script.debug) console.log(e);
			if (this.value !== "" && e.keyCode === 13) {
				sendSearchRequest(this.value);
			} else if (this.value !== "" && e.keyCode === 8) {
				searchSuggestions(this.value);
			} else {
				searchSuggestions(this.value + e.key);
			}
		});

		input.addEventListener('click', function(event) {
			this.select();
		});
	}

	// callback from search suggestions attached to window
	function search_callback(data) {
		var raw = data[1];
		script.suggestions = raw.map(function(array) {
			return array[0];
		});
		if (script.debug) console.log(script.suggestions);
	}

	// get search suggestions
	function searchSuggestions(value) {
		if (script.search_timeout !== null) clearTimeout(script.search_timeout);

		script.search_timeout = setTimeout( function() {
			if (script.debug) console.log("search request send");
			var s = document.createElement('script');
			s.type = 'text/javascript';
			s.src = 'https://clients1.google.com/complete/search?client=youtube&hl=en&gl=be&gs_ri=youtube&ds=yt&q=' + encodeURIComponent(value) + '&callback=search_callback';
			var h = document.getElementsByTagName('script')[0];
			h.parentNode.insertBefore(s, h);
		}.bind(value), 100);
	}

	// send search request
	function sendSearchRequest(value) {
		if (script.debug) console.log("searching for " + value);
		
		document.getElementById("masthead-queueSearch").blur();

		var nextPage = document.getElementById("watch-more-related-button");
		if (nextPage !== null) nextPage.parentNode.removeChild(nextPage);

		script.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);
	}

	// process search request
	function processSearch(value) {
		var videoList = value.getElementsByClassName("item-section")[0];

		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();
			}
		}

		var videos = videoList.querySelectorAll('.yt-lockup-video');
		for (var j = videos.length - 1; j >= 0; j--) {
			var video = videos[j];
			var videoId = video.dataset.contextItemId;
			var videoTitle = video.querySelector('.yt-lockup-title > a').title;
			var videoStats = video.querySelector('.yt-lockup-meta-info').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);
		}

		// next page results
		var disabledButton = value.querySelector("button.yt-uix-button-default[disabled=true]");
		var nextPageButton = disabledButton.nextElementSibling;

		findVideoThumbs();
	}

	// *** 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(time) {
			setCache("queue", this.get());
			if (script.debug) console.log(this.get().slice());
			setTimeout(function() {displayQueue();}, time);
		};
	}

	// *** Video *** //

	// play next video behavior depending on if you're watching fullscreen
	function playNextVideo(nextVidId) {
		if (script.debug) console.log("playing next song");
		if (isPlayerFullscreen()) {
			script.ytplayer.loadVideoById(nextVidId, 0);
		} else {
			window.spf.navigate("https://www.youtube.com/watch?v=" + nextVidId + "&t=0s");
		}
	}

	// finding video's that you can add to the queue
	function findVideoThumbs() {
		var videos = document.querySelectorAll(".related-list-item:not(.processed-buttons)");
		for (var j = 0; j < videos.length; j++) {
			var video = findVideoInformation(videos[j], "#watch-related");
			videos[j].classList.add("processed-buttons");
			if (video) {
				addButton(video);
			}
		}
	}

	// extracting video information and creating a video object (that can be added to the queue)
	function findVideoInformation(video, query) {
		var anchor = video.querySelector(query + " .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 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(".youtubequeue-add");
				if (addedButton)
					addedButton.parentNode.parentNode.removeChild(addedButton.parentNode);

				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 *** //

	// The "add to queue" button
	function addButton(video) {
		var anchor = video.buttonAnchor;
		var html = '<div class="yt-uix-button yt-uix-button-default yt-uix-button-size-default" style="height:initial; padding:3px"><button class="yt-uix-button-content youtubequeue-add">Add to queue</button></div>';
		anchor.insertAdjacentHTML('beforeend', html);

		anchor.querySelector(".youtubequeue-add").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="yt-uix-button yt-uix-button-default yt-uix-button-size-default" style="height:initial; padding:3px"><button class="yt-uix-button-content youtubequeue-add">Add to queue</button></div>';
		anchor.insertAdjacentHTML('beforeend', html);

		anchor.querySelector(".youtubequeue-add").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="yt-uix-button yt-uix-button-default yt-uix-button-size-default" style="height:initial; padding:3px; margin-left:3px"><button class="yt-uix-button-content youtubequeue-remove">Remove</button></div>';
		anchor.insertAdjacentHTML("beforeend", html);

		anchor.querySelector(".youtubequeue-remove").addEventListener('click', function handler(e) {
			e.preventDefault();
			this.textContent = "Removed!";
			script.queue.remove(nb);
			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="yt-uix-button yt-uix-button-default yt-uix-button-size-default" style="height:initial; padding:3px"><button class="yt-uix-button-content youtubequeue-next">Play Next</button></div>';
		anchor.insertAdjacentHTML("beforeend", html);

		anchor.querySelector(".youtubequeue-next").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="yt-uix-button yt-uix-button-default yt-uix-button-size-default" style="height:initial; padding:3px"><button class="yt-uix-button-content youtubequeue-now">Play Now</button></div>';
		anchor.insertAdjacentHTML("beforeend", html);

		anchor.querySelector(".youtubequeue-now").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="yt-uix-button yt-uix-button-default yt-uix-button-size-default" style="height:initial; padding:3px"><button class="yt-uix-button-content youtubequeue-remove-list">Remove Queue</button></div>';
		anchor.innerHTML = html;

		anchor.querySelector(".youtubequeue-remove-list").addEventListener("click", function handler(e) {
			e.preventDefault();
			this.textContent = "Removed!";
			script.queue.reset();
			e.currentTarget.removeEventListener(e.type, handler);
			this.addEventListener("click", function (e) {
				e.preventDefault();
			});
		});
	}

	// *** GETTERS *** //

	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];
			}
		}
	}

	// *** 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 *** //

	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 += "				   <ul class=\"yt-lockup-meta-info stat\">" + video.stats + "<\/ul>";
		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;
	}
})();