您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Adds button for loading the previous video in a YT playlist
当前为
// ==UserScript== // @name Play Youtube playlist in reverse order // @namespace https://github.com/Dragosarus/Userscripts/ // @version 7.2 // @description Adds button for loading the previous video in a YT playlist // @author Dragosarus // @match http://www.youtube.com/* // @match https://www.youtube.com/* // @grant none // @require http://code.jquery.com/jquery-latest.js // @noframes // ==/UserScript== // Cookies (current session): // pytplir_playPrevious - saves the button state between loads /* NOTES: * - If the button is not displayed (but the script is running), pause and unpause the video. * - If it still does not appear, reload the page. * - If it *still* does not appear, let me know through Greasy Fork or GitHub. * - If the button is displayed but does not work properly/consistently, increase the value of redirectWhenTimeLeft. */ (function() { 'use strict'; $(document).ready(function() { // Determines when to load the next video. // Increase these if the redirect does not work as intended (i.e. fails to override Youtube's redirect), // Decreasing these will let you see more of the video before it redirects, but the redirect might stop working (consistently) var redirectWhenTimeLeft = 0.3; // seconds before the end of the video var redirectWhenTimeLeft_miniplayer = 0.6; var skipPremiere = true; // Skip videos that have not been premiered yet var activeColor = "rgb(64,166,255)"; var inactiveColor = "rgb(144,144,144)"; var circleColor = "rgb(144,144,144)"; var ttBGColor = "rgb(100,100,100)"; var ttTextColor = "rgb(237,240,243)"; var selectors = { "buttonLocation": ".ytd-playlist-panel-renderer > div[id=top-level-buttons-computed]", "content": "#content", "player": ".html5-main-video", "miniplayerDiv": "div.miniplayer", "playlistButtons": ".ytd-watch-flexy #playlist #playlist-action-menu", "playlistButtonsMiniplayer": "ytd-playlist-panel-renderer.ytd-miniplayer #playlist-action-menu", "playlistCurrentVideo": "ytd-playlist-panel-video-renderer[selected]", "playlistVideos": "#publisher-container span.index-message", "playlistVideosMiniplayer": "yt-formatted-string[id=owner-name] :nth-child(3)", "shuffleButtonActive": "path[d='M18.51,13.29l4.21,4.21l-4.21,4.21l-1.41-1.41l1.8-1.8c-2.95-0.03-5.73-1.32-7.66-3.55l1.51-1.31 c1.54,1.79,3.77,2.82,6.13,2.85l-1.79-1.79L18.51,13.29z M18.88,7.51l-1.78,1.78l1.41,1.41l4.21-4.21l-4.21-4.21l-1.41,1.41l1.8,1.8 c-3.72,0.04-7.12,2.07-8.9,5.34l-0.73,1.34C7.81,14.85,5.03,17,2,17v2c3.76,0,7.21-2.55,9.01-5.85l0.73-1.34 C13.17,9.19,15.9,7.55,18.88,7.51z M8.21,10.31l1.5-1.32C7.77,6.77,4.95,5,2,5v2C4.38,7,6.64,8.53,8.21,10.31z']", "shuffleButtonInactive": "path[d='M18.15,13.65l3.85,3.85l-3.85,3.85l-0.71-0.71L20.09,18H19c-2.84,0-5.53-1.23-7.39-3.38l0.76-0.65 C14.03,15.89,16.45,17,19,17h1.09l-2.65-2.65L18.15,13.65z M19,7h1.09l-2.65,2.65l0.71,0.71l3.85-3.85l-3.85-3.85l-0.71,0.71 L20.09,6H19c-3.58,0-6.86,1.95-8.57,5.09l-0.73,1.34C8.16,15.25,5.21,17,2,17v1c3.58,0,6.86-1.95,8.57-5.09l0.73-1.34 C12.84,8.75,15.79,7,19,7z M8.59,9.98l0.75-0.66C7.49,7.21,4.81,6,2,6v1C4.52,7,6.92,8.09,8.59,9.98z']", "shuffleButtonLegacy": "path[d='M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z']", "timestamp": "span.ytd-thumbnail-overlay-time-status-renderer", "videoPlayer": ".html5-video-player" } var debug = false; var player; var playPrevious; var ytdApp = $("ytd-app")[0]; var redirectFlag = false; var vidNum; // string var shuffle; var miniplayerFlag = false; // keep track of switches between miniplayer and normal mode var playerListenersAdded = false; // create button var svgNS = "http://www.w3.org/2000/svg"; var btn_div = document.createElement("div"); var bg_circle = document.createElementNS(svgNS, "circle"); var bg_circle_anim = document.createElementNS(svgNS, "animate"); var arrow_up = document.createElementNS(svgNS, "polygon"); var arrow_down = document.createElementNS(svgNS, "polygon"); var btn_svg = document.createElementNS(svgNS, "svg"); var tt_svg = document.createElementNS(svgNS, "svg"); var tt_svg_fadein = document.createElementNS(svgNS, "animate"); var tt_svg_fadeout = document.createElementNS(svgNS, "animate"); var tt_rect = document.createElementNS(svgNS, "rect"); var tt_text = document.createElementNS(svgNS, "text"); var tt_div = document.createElement("div"); setAttributes(bg_circle_anim, [["attributeName", "fill-opacity"], ["values", "0;0.1;0.2;0.1;0.0"], ["dur", "0.3s"], ["restart", "always"], ["repeatCount", "1"], ["begin", "indefinite"], ["id", "pytplir_bg_circle_anim"]]); setAttributes(bg_circle, [["cx", "20"], ["cy", "20"], ["r", "20"], ["fill", circleColor], ["fill-opacity", "0"]]); setAttributes(arrow_up, [["points", "17,19 17,17 13,17 20,11 27,17 23,17 23,19"], ["id", "pytplir_arrow_up"]]); setAttributes(arrow_down, [["points", "17,21 17,23 13,23 20,29 27,23 23,23 23,21"], ["id", "pytplir_arrow_down"]]); setAttributes(btn_svg, [["xmlns", svgNS], ["viewbox", "0 0 40 40"], ["width", "40"], ["height", "40"], ["style", "cursor: pointer; margin-left: 8px;"], ["id", "pytplir_btn"]]); setAttributes(tt_rect, [["x", "0"], ["y", "0"], ["rx", "2"], ["ry", "2"], ["width", "110"], ["height", "34"], ["fill", ttBGColor], ["fill-opacity", "0.9"]]); setAttributes(tt_text, [["x", "8"], ["y", "22"], ["font-family", "Roboto, Noto, sans-serif"], ["font-size", "13px"], ["fill", ttTextColor], ["style", "user-select:none;"]]); setAttributes(tt_svg_fadein, [["attributeType", "CSS"], ["attributeName", "opacity"], ["values", "0;1"], ["dur", "0.1s"], ["restart", "always"], ["repeatCount", "1"], ["begin", "indefinite"], ["id", "pytplir_tt_fadein"], ["fill", "freeze"]]); setAttributes(tt_svg_fadeout, [["attributeType", "CSS"], ["attributeName", "opacity"], ["values", "1;0"], ["dur", "0.1s"], ["restart", "always"], ["repeatCount", "1"], ["begin", "indefinite"], ["id", "pytplir_tt_fadeout"], ["fill", "freeze"]]); var tt_svg_offset = "position:absolute; top:13px; left:-32px; z-index:100; opacity:0.0;"; setAttributes(tt_svg, [["viewbox", "0 0 100 34"], ["xmlns", "http://www.w3.org/2000/svg"], ["width", "100"], ["height", "34"], ["style", "padding-left: 10px; fill:" + ttBGColor + "; " + tt_svg_offset], ["id", "pytplir_tt"]]); setAttributes(tt_div, [["style", "position:relative; width:0; height:0;"]]); setAttributes(btn_div, [["id", "pytplir_div"]]); tt_text.innerHTML = "Autoplay order"; bg_circle.appendChild(bg_circle_anim); appendChildren(btn_svg, [bg_circle, arrow_up, arrow_down]); appendChildren(tt_svg, [tt_rect, tt_text, tt_svg_fadein, tt_svg_fadeout]); tt_div.appendChild(tt_svg); appendChildren(btn_div, [btn_svg, tt_div]); $(btn_svg).on("click", onButtonClick); $(btn_svg).on("click", function(){$(this).parent().find("#pytplir_bg_circle_anim")[0].beginElement();}); $(btn_svg).on("mouseenter", function(){$(this).parent().find("#pytplir_tt_fadein")[0].beginElement();}); $(btn_svg).on("mouseleave", function(){$(this).parent().find("#pytplir_tt_fadeout")[0].beginElement();}); init(); function setAttributes(node, attributeValuePairs) { // [["id", "example"], ["width","20"], ...] for (var attVal of attributeValuePairs){ node.setAttribute(attVal[0], attVal[1]); } } function appendChildren(node, childList) { for (var child of childList) { node.appendChild(child); } } function init() { // the button needs to be re-added whenever the playlist is updated (e.g when a video is loaded or removed) function observerCallback(mutationList, observer) { debugLog("Observer triggered!") start(); } const playlistObserver = new MutationObserver(observerCallback); const observerOptions = {subtree:true, childList:true, characterData:true}; initObserver(playlistObserver, observerOptions); playPrevious = getCookie("pytplir_playPrevious"); if (playPrevious === "") { // cookie has not been set yet playPrevious = false; // inital state setCookie("pytplir_playPrevious", playPrevious); } start(); } function initObserver(observer, options) { try { observer.observe($(selectors.playlistVideos)[0], options); observer.observe($(selectors.playlistVideosMiniplayer)[0], options); } catch (e) { setTimeout(function(){initObserver(observer)}, 100); } } function onButtonClick() { // toggle playPrevious = !playPrevious; setCookie("pytplir_playPrevious", playPrevious); updateButtonState(); } function addButton() { // Add button(s) debugLog("addButton start") withQuery(selectors.buttonLocation, "*", function(res) { res.each(function() { if (!$(this).find("#pytplir_div").length) { this.appendChild($(btn_div).clone(true)[0]); updateButtonState(); debugLog("button added"); } }); }); debugLog("addButton finish") } function updateButtonState() { if (playPrevious) { // play previous video $("polygon[id=pytplir_arrow_up]").each(function() { this.setAttribute("style", "fill:" + activeColor); }); $("polygon[id=pytplir_arrow_down]").each(function() { this.setAttribute("style", "fill:" + inactiveColor); }); } else { // play next video $("polygon[id=pytplir_arrow_up]").each(function() { this.setAttribute("style", "fill:" + inactiveColor); }); $("polygon[id=pytplir_arrow_down]").each(function() { this.setAttribute("style", "fill:" + activeColor); }); } $("#pytplir_btn")[0].setAttribute("activated", playPrevious); } function start() { // Add button(s) and event listeners addButton(); debugLog("playerListenersAdded = " + playerListenersAdded); if (!playerListenersAdded) { withQuery(selectors.player, ":visible", function(res) { player = res[0]; player.addEventListener("timeupdate", checkTime); player.addEventListener("play", addButton); // ensure button is added playerListenersAdded = true; }); } } function withQuery(query, filter="*", onSuccess = function(r){}) { var res; if (filter == "*") { res = $(query); } else { res = $(query).filter(filter); } if (res.length) { // >= 1 result onSuccess(res); return res; } else { // not loaded yet => retry setTimeout(function(){withQuery(query, filter, onSuccess)}); } } function checkTime() { var miniplayerActive = ytdApp.hasAttribute("miniplayer-active_"); var context = miniplayerActive ? selectors.miniplayerDiv : selectors.content; var buttonSelector = context + " " + selectors.buttonLocation + " #pytplir_div"; var noButton = !$(buttonSelector).length; var playlistHeaderQuery = miniplayerActive ? $(selectors.playlistVideosMiniplayer).parent() : $(selectors.playlistVideos).parent(); var playlistVisible = playlistHeaderQuery.length && playlistHeaderQuery.is(":visible"); // exit early when not watching a playlist if (!playlistVisible) {return;} // button not loaded else if (noButton) { // button was removed debugLog("failsafe: adding button"); addButton(); } debugLog("checkTime: miniplayer: " + miniplayerActive + ", button == " + !noButton); var timeLeft = player.duration - player.currentTime; var videoPlayer = $(selectors.videoPlayer)[0]; if (!shuffle || (miniplayerActive != miniplayerFlag)) { // wysiwyg shuffle = $(selectors.shuffleButtonActive).parents("button[aria-pressed]"); if (!shuffle.length) { // shuffle not activated or new UI has not been pushed to the user yet shuffle = $(selectors.shuffleButtonInactive).parents("button[aria-pressed]"); if (!shuffle.length) { // new UI not pushed to user shuffle = $(selectors.shuffleButtonLegacy).filter(":visible").parents("button[aria-pressed]"); } } shuffle = shuffle[0]; } var miniplayerFlag = miniplayerActive; try {videoPlayer.classList.contains("ad-showing");} // ensure it will work below catch (TypeError) { // video player undefined return; } var redirectTime; if (miniplayerActive) { redirectTime = redirectWhenTimeLeft_miniplayer; } else { redirectTime = redirectWhenTimeLeft; } var shuffleEnabled = strToBool(shuffle.attributes["aria-pressed"].nodeValue); if (timeLeft < redirectTime && !redirectFlag && playPrevious && !shuffleEnabled && !player.hasAttribute("loop") && !videoPlayer.classList.contains("ad-showing")) { // attempt to prevent the default redirect from triggering player.pause(); player.currentTime -= 2; if (getVidNum()[0] != 1) { redirectFlag = true; redirect(); setTimeout(function() {redirectFlag = false;}, 1000); } } } function getVidNum() { // returns integer array [current, total], e.g "32 / 152" => [32, 152] var vidNum_tmp; if (ytdApp.hasAttribute("miniplayer-active_")) { vidNum_tmp = $(selectors.playlistVideosMiniplayer)[0].innerHTML; } else { vidNum_tmp = $(selectors.playlistVideos)[0].innerHTML; } return vidNum_tmp.split(" / ").map(x => parseInt(x)); } function redirect() { var previousURL = getPreviousURL(); if (previousURL) { previousURL.click(); } } function getPreviousURL(){ // returns <a> element var elem; if (ytdApp.hasAttribute("miniplayer-active_")) { // avoid being forced out of miniplayer mode on video load elem = $(selectors.miniplayerDiv).find(selectors.playlistCurrentVideo).prev(); } else { elem = $(selectors.content).find(selectors.playlistCurrentVideo).prev(); } if (skipPremiere) { var ts = $(elem).find(selectors.timestamp); if (ts.length) {ts = ts[0].innerHTML; } } while (!elem.find("#unplayableText").prop("hidden") || (skipPremiere && typeof(ts) == "string" && !ts.includes(":"))) { // while an unplayable (e.g. private) video is selected elem = elem.prev(); if (skipPremiere) { ts = $(elem).find(selectors.timestamp); if (ts.length) { ts = ts[0].innerHTML; } } } return elem.children()[0]; } function strToBool(str) { return str.toLowerCase() == "true"; } function debugLog(msg){ if (debug) { console.log("pytplir: " + msg); }; } // adapted from https://www.w3schools.com/js/js_cookies.asp function setCookie(cname, cvalue) { document.cookie = cname + "=" + cvalue + ";sameSite=lax;path=www.youtube.com/watch"; } function getCookie(cname) { var name = cname + "="; var decodedCookie = decodeURIComponent(document.cookie); var ca = decodedCookie.split(';'); for(var i = 0; i <ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') { c = c.substring(1); } if (c.indexOf(name) == 0) { var x = c.substring(name.length, c.length); return strToBool(x); } } return ""; } }); })(); /*eslint-env jquery*/ // stop eslint from showing "'$' is not defined" warnings