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!

目前为 2017-04-25 提交的版本。查看 最新版本

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

QingJ © 2025

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