Play Youtube playlist in reverse order

Adds button for loading the previous video in a YT playlist

目前為 2020-06-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 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      2.1
// @description  Adds button for loading the previous video in a YT playlist
// @author       Dragosarus
// @match        www.youtube.com/watch?*list*
// @grant        none
// @require      http://code.jquery.com/jquery-latest.js
// ==/UserScript==

// Cookies (current session):
// pytplir_playPrevious - saves the button state between loads

(function() {
    'use strict';

    // Your code here...
    $(document).ready(function() {
        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 arrow_up;
        var arrow_down;
        var btn_svg;
        var btn_div;
        var playPrevious;
        var redirectFlag = false;
        // remove the need to refresh the page for the script to work properly
        $("html")[0].addEventListener("yt-navigate-finish",init);
        $("html")[0].addEventListener("yt-navigate-start",removeButton);

        init();

        function init() {
            playPrevious = getCookie("pytplir_playPrevious");
            if (playPrevious === "") { // cookie has not been set yet
                playPrevious = false; // inital state
                setCookie("pytplir_playPrevious",playPrevious);
            }

            // create button
            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");
            arrow_up = document.createElementNS("http://www.w3.org/2000/svg","polygon");
            arrow_down = document.createElementNS("http://www.w3.org/2000/svg","polygon");
            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");

            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","pytplir_btn.click");
            bg_circle_anim.setAttribute("end","pytplir_btn.click");
            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_down.setAttribute("points","17,21 17,23 13,23 20,29 27,23 23,23 23,21");

            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.addEventListener("click",onButtonClick);
            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","pytplir_btn.mouseenter");
            tt_svg_fadein.setAttribute("end","pytplir_btn.mouseleave");
            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","pytplir_btn.mouseleave");
            tt_svg_fadeout.setAttribute("end","pytplir_btn.mouseenter");
            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:relative; top:50px; left:-80px; 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);

            btn_div.setAttribute("id","pytplir_div");
            btn_div.appendChild(btn_svg);
            btn_div.appendChild(tt_svg);

            setTimeout(addButton, 500);
            setTimeout(start, 500);
        }

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

        function addButton() {
            withQuery(".ytd-playlist-panel-renderer > #top-level-buttons", function(res) {
                if (!document.getElementById("pytplir_btn")) {
                    res[0].appendChild(btn_div);
                    updateButtonState();
                }
            });
        }

        function updateButtonState() {
            if (playPrevious) { // play previous video
                arrow_up.setAttribute("style","fill:"+activeColor);
                arrow_down.setAttribute("style","fill:"+inactiveColor);
            } else { // play next video
                arrow_up.setAttribute("style","fill:"+inactiveColor);
                arrow_down.setAttribute("style","fill:"+activeColor);
            }
        }

        function removeButton() {
            btn_svg.parentNode.removeChild(btn_svg);
        }

        function start() {
            withQuery(".html5-main-video", function(res) {
                player = res[0];
                player.addEventListener("timeupdate",checkTime);
            });
        }

        function withQuery(query, onSuccess) {
            var res = $(query);
            if (res) {
                onSuccess(res);
                return res;
            } else { // not loaded yet => retry
                setTimeout(function(){withQuery(query);}, 100);
            }
        }

        function checkTime() {
            var timeLeft = player.duration - player.currentTime;
            var shuffle = strToBool($("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']")[1].parentNode.parentNode.parentNode.parentNode.attributes["aria-pressed"].nodeValue);
            if (playPrevious && timeLeft < 1.3 && !redirectFlag && !player.hasAttribute("loop") && !shuffle) {
                redirectFlag = true;
                redirect();
                setTimeout(function() {redirectFlag = false;}, 1000);
            }
        }

        function redirect() {
            var previousURL = getPreviousURL();
            if (previousURL) {
                document.location.href = previousURL;
            } else { // probably at start of playlist
                player.pause(); // prevent next video from loading
            }
        }

        function getPreviousURL(){
            var query = document.querySelectorAll("#playlist-items");
            var index;
            var previousURL;
            for (const a of Array.from(query).entries()) { // a = {index, element}
                if (a[1].textContent.includes("▶")) { // Current video's position in playlist
                    index = a[0]; // index in query
                    if (index == 0) {break;} // start of list
                    previousURL = query[index-1].children[0].href;
                    return previousURL;
                }
            }
        }

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

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