Play Youtube playlist in reverse order

Adds button for loading the previous video in a YT playlist

当前为 2020-08-26 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Play Youtube playlist in reverse order
// @namespace    https://github.com/Dragosarus/Userscripts/
// @version      5.2
// @description  Adds button for loading the previous video in a YT playlist
// @author       Dragosarus
// @match        http*://www.youtube.com/*
// @grant        none
// @require      http://code.jquery.com/jquery-latest.js
// ==/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 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 end of 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 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 btn_div = document.createElement("div");
        var bg_circle = document.createElementNS("http://www.w3.org/2000/svg","circle");
        var bg_circle_anim = document.createElementNS("http://www.w3.org/2000/svg","animate");
        var arrow_up = document.createElementNS("http://www.w3.org/2000/svg","polygon");
        var arrow_down = document.createElementNS("http://www.w3.org/2000/svg","polygon");
        var btn_svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
        var tt_svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
        var tt_svg_fadein = document.createElementNS("http://www.w3.org/2000/svg","animate");
        var tt_svg_fadeout = document.createElementNS("http://www.w3.org/2000/svg","animate");
        var tt_rect = document.createElementNS("http://www.w3.org/2000/svg","rect");
        var tt_text = document.createElementNS("http://www.w3.org/2000/svg","text");
        var tt_div = document.createElement("div");

        bg_circle_anim.setAttribute("attributeName","fill-opacity");
        bg_circle_anim.setAttribute("values","0;0.1;0.2;0.1;0.0");
        bg_circle_anim.setAttribute("dur","0.3s");
        bg_circle_anim.setAttribute("restart","always");
        bg_circle_anim.setAttribute("repeatCount","1");
        bg_circle_anim.setAttribute("begin","indefinite");
        bg_circle_anim.setAttribute("id","pytplir_bg_circle_anim");
        bg_circle.setAttribute("cx","20");
        bg_circle.setAttribute("cy","20");
        bg_circle.setAttribute("r","20");
        bg_circle.setAttribute("fill",circleColor);
        bg_circle.setAttribute("fill-opacity","0");
        bg_circle.appendChild(bg_circle_anim);
        arrow_up.setAttribute("points","17,19 17,17 13,17 20,11 27,17 23,17 23,19");
        arrow_up.setAttribute("id","pytplir_arrow_up");
        arrow_down.setAttribute("points","17,21 17,23 13,23 20,29 27,23 23,23 23,21");
        arrow_down.setAttribute("id","pytplir_arrow_down");

        btn_svg.setAttribute("viewbox","0 0 40 40");
        btn_svg.setAttribute("xmlns","http://www.w3.org/2000/svg");
        btn_svg.setAttribute("width","40");
        btn_svg.setAttribute("height","40");
        btn_svg.setAttribute("style","cursor: pointer; margin-left: 8px;");
        btn_svg.setAttribute("id","pytplir_btn");
        btn_svg.appendChild(bg_circle);
        btn_svg.appendChild(arrow_up);
        btn_svg.appendChild(arrow_down);

        tt_rect.setAttribute("x","0");
        tt_rect.setAttribute("y","0");
        tt_rect.setAttribute("rx","2");
        tt_rect.setAttribute("ry","2");
        tt_rect.setAttribute("width","110");
        tt_rect.setAttribute("height","34");
        tt_rect.setAttribute("fill",ttBGColor);
        tt_rect.setAttribute("fill-opacity","0.9");

        tt_text.setAttribute("x","8");
        tt_text.setAttribute("y","22");
        tt_text.setAttribute("font-family","Roboto, Noto, sans-serif");
        tt_text.setAttribute("font-size","13px");
        tt_text.setAttribute("fill",ttTextColor);
        tt_text.setAttribute("style","user-select:none;");
        tt_text.innerHTML = "Autoplay order";

        tt_svg_fadein.setAttribute("attributeType","CSS");
        tt_svg_fadein.setAttribute("attributeName","opacity");
        tt_svg_fadein.setAttribute("values","0;1");
        tt_svg_fadein.setAttribute("dur","0.1s");
        tt_svg_fadein.setAttribute("restart","always");
        tt_svg_fadein.setAttribute("repeatCount","1");
        tt_svg_fadein.setAttribute("begin","indefinite");
        tt_svg_fadein.setAttribute("id","pytplir_tt_fadein");
        tt_svg_fadein.setAttribute("fill","freeze");
        tt_svg_fadeout.setAttribute("attributeType","CSS");
        tt_svg_fadeout.setAttribute("attributeName","opacity");
        tt_svg_fadeout.setAttribute("values","1;0");
        tt_svg_fadeout.setAttribute("dur","0.1s");
        tt_svg_fadeout.setAttribute("restart","always");
        tt_svg_fadeout.setAttribute("repeatCount","1");
        tt_svg_fadeout.setAttribute("begin","indefinite");
        tt_svg_fadeout.setAttribute("id","pytplir_tt_fadeout");
        tt_svg_fadeout.setAttribute("fill","freeze");
        tt_svg.setAttribute("viewbox","0 0 100 34");
        tt_svg.setAttribute("xmlns","http://www.w3.org/2000/svg");
        tt_svg.setAttribute("width","100");
        tt_svg.setAttribute("height","34");
        var tt_svg_offset = "position:absolute; top:13px; left:-32px; z-index:100; opacity:0.0;";
        tt_svg.setAttribute("style","padding-left: 10px; fill:" + ttBGColor + "; " + tt_svg_offset);
        tt_svg.setAttribute("id","pytplir_tt");
        tt_svg.appendChild(tt_rect);
        tt_svg.appendChild(tt_text);
        tt_svg.appendChild(tt_svg_fadein);
        tt_svg.appendChild(tt_svg_fadeout);
        tt_div.setAttribute("style", "position:relative; width:0; height:0;");
        tt_div.appendChild(tt_svg);

        btn_div.setAttribute("id","pytplir_div");
        btn_div.appendChild(btn_svg);
        btn_div.appendChild(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 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) {
                start();
            }
            const playlistObserver = new MutationObserver(observerCallback);
            const observerOptions = {subtree:true, childList:true, attributes:true, characterData:true};
            initObservers(playlistObserver, observerOptions);
            playPrevious = getCookie("pytplir_playPrevious");
            if (playPrevious === "") { // cookie has not been set yet
                playPrevious = false; // inital state
                setCookie("pytplir_playPrevious",playPrevious);
            }
            start();
        }

        function initObservers(observer, options) {
            try {
                observer.observe($(".ytd-watch-flexy #playlist").find("#playlist-action-menu")[0], options);
                observer.observe($(".miniplayer #header-contents")[0],options);
            } catch (e) {
                setTimeout(function(){initObservers(observer)},100);
            }
        }

        function onButtonClick() { // toggle
            playPrevious = !playPrevious;
            setCookie("pytplir_playPrevious",playPrevious);
            updateButtonState();
        }

        function addButton() {
            withQuery(".ytd-playlist-panel-renderer > div[id=top-level-buttons]", "*", function(res) {
                res.each(function() {
                    if (!$(this).find("#pytplir_div").length) {
                        this.appendChild($(btn_div).clone(true)[0]);
                        updateButtonState();
                    }
                });
            });
        }

        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() {
            addButton();
            if (!playerListenersAdded) {
                withQuery(".html5-main-video", ":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);}, 100);
            }
        }

        function checkTime() {
            if (!$("#pytplir_div").length) {return;} // button not loaded

            var timeLeft = player.duration - player.currentTime;
            var videoPlayer = $(".html5-video-player")[0];
            var miniplayerActive = ytdApp.hasAttribute("miniplayer-active_");
            if (!shuffle || (miniplayerActive != miniplayerFlag)) { // wysiwyg
                shuffle = $("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']").filter(":visible").parents("button[aria-pressed]")[0];
            }
            var miniplayerFlag = miniplayerActive;
            var shuffleEnabled = strToBool(shuffle.attributes["aria-pressed"].nodeValue);
            try {videoPlayer.classList.contains("ad-showing");}
            catch (TypeError) { // video player undefined
            	return;
            }

            var redirectTime;
            if (miniplayerActive) {
                redirectTime = redirectWhenTimeLeft_miniplayer;
            } else {
                redirectTime = redirectWhenTimeLeft;
            }

            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() != "1") {
                    redirectFlag = true;
                    redirect();
                    setTimeout(function() {redirectFlag = false;}, 1000);
                }
            }
        }

        function getVidNum() {
            var vidNum_tmp;
            if (ytdApp.hasAttribute("miniplayer-active_")) {
                vidNum_tmp = $("yt-formatted-string[id=owner-name").children()[2].innerHTML;
            } else {
                vidNum_tmp = $("#publisher-container").find("span")[1].innerHTML + "/"; // apparently this is e.g "1" in Firefox, but "1 / n" in Chrome
            }

            return $.trim(vidNum_tmp.substring(0,vidNum_tmp.indexOf("/")));
        }

        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 = $("div.miniplayer").find("ytd-playlist-panel-video-renderer[selected]").prev();
            } else {
                elem = $("#content").find("ytd-playlist-panel-video-renderer[selected]").prev();
            }

            if (skipPremiere) {
                var ts = $(elem).find("span.ytd-thumbnail-overlay-time-status-renderer");
                if (ts.length) {ts = ts[0].innerHTML; }
            }

            while (!elem.find("#unplayableText").prop("hidden") || (skipPremiere && !ts.includes(":"))) { // while unplayable (e.g. private) video is selected
                elem = elem.prev();
                if (skipPremiere) {
                    ts = $(elem).find("span.ytd-thumbnail-overlay-time-status-renderer");
                    if (ts.length) { ts = ts[0].innerHTML; }
                }
            }
            return elem.children()[0];
        }

        function strToBool(str) {
            return str.toLowerCase() == "true";
        }

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