Play Youtube playlist in reverse order

Adds button for loading the previous video in a YT playlist

目前為 2020-08-19 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Play Youtube playlist in reverse order
// @namespace    https://github.com/Dragosarus/Userscripts/
// @version      5.0
// @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 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);
            }
            setTimeout(start, 500);
        }

        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();
            }
            while (!elem.find("#unplayableText").prop("hidden")) { // while unplayable (e.g. private) video is selected
                elem = elem.prev();
            }
            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 "";
        }
    });
})();