YouTube 以播放時間長度排序播放清單

使用官方API以播放時間長度排序清單

目前為 2023-10-26 提交的版本,檢視 最新版本

// ==UserScript==

// @name               YouTube sort playlists by play time length
// @name:zh-TW         YouTube 以播放時間長度排序播放清單
// @name:zh-CN         YouTube 以播放时间长度排序播放清单
// @name:ja            YouTube でプレイリストを再生時間順に並べ替える
// @description        Sorting playlists by play time length use internal API .
// @description:zh-TW  使用官方API以播放時間長度排序清單
// @description:zh-CN  使用官方API以播放时间长度排序清单
// @description:ja     再生時間の長さによるプレイリストの並べ替えには、内部 API を使用します。
// @copyright 2023, HrJasn (https://github.com/HrJasn/)
// @license MIT
// @icon
// @homepageURL https://github.com/HrJasn/
// @supportURL https://github.com/HrJasn/
// @contributionURL https://github.com/HrJasn/
// @version 2.2
// @namespace https://github.com/HrJasn/
// @grant          none
// @match        http*://www.youtube.com/*
// @exclude        http*://www.google.com/*

// ==/UserScript==

(() => {
    'use strict';
    window.onload = () => {
        let setCookie = (name,value,days) => {
            let expires = "";
            if (days) {
                let date = new Date();
                date.setTime(date.getTime() + (days*24*60*60*1000));
                expires = "; expires=" + date.toUTCString();
            }
            document.cookie = name + "=" + (value || "") + expires + "; path=/";
        };
        let getCookie=(name) =>{
            let nameEQ = name + "=";
            let ca = document.cookie.split(';');
            for(let i=0;i < ca.length;i++) {
                let c = ca[i];
                while (c.charAt(0)==' ') c = c.substring(1,c.length);
                if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
            }
            return null;
        };
        let eraseCookie=(name) =>{
            document.cookie = name +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
        };
        if (typeof Array.prototype.equals === "undefined") {
            Array.prototype.equals = function( array ) {
                return this.length == array.length &&
                    this.every( function(this_i,i) { return this_i == array[i] } )
            };
        };
        if (typeof Array.prototype.move === "undefined") {
            Array.prototype.move = function(from, to, on = 1) {
                return this.splice(to, 0, ...this.splice(from, on)), this
            };
        };
        let MutationObserverTimer, observer;
        observer = new MutationObserver( (mutations) => {
            if(MutationObserverTimer){
                clearTimeout(MutationObserverTimer);
            };
            MutationObserverTimer = setTimeout(() => {
                console.log("YouTube sort playlists by play time length is loading.");
                let ypvlse = document.querySelector('div#icon-label');
                let ypvlmtArr = {
                    'en':'Play time length',
                    'zh-TW':'播放時長',
                    'zh-CN':'播放时长',
                    'ja': 'プレイ時間'
                };
                let ypvlmt = ypvlmtArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)];
                if(![...ypvlse.parentNode.children].find(cn => cn.innerText.match(ypvlmt))){
                    let yvlmen = '';
                    if(!(getCookie('CustomSortStatus'))){
                        yvlmen = ypvlmt + '↑↓';
                    } else {
                        yvlmen = JSON.parse(getCookie('CustomSortStatus')).BtnStr;
                    };
                    const ypvlme = ypvlse.cloneNode(true);
                    ypvlme.innerHTML = yvlmen;
                    ypvlse.parentNode.insertBefore(ypvlme,ypvlse.nextSibling);
                    let MutationObserverTimer3;
                    ypvlme.addEventListener('click',(evnt) => {
                        observer.disconnect();
                        evnt.preventDefault();
                        evnt.stopPropagation();
                        evnt.stopImmediatePropagation();
                        let ypvricarr = [...ytInitialData.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].playlistVideoListRenderer.contents];
                        let ypvrearr = [];
                        let orgetih = evnt.target.innerHTML;
                        if(evnt.target.innerHTML == (' ' + ypvlmt + '↑')){
                            ypvrearr = [...ypvricarr].sort((a,b)=>{
                                return parseInt(b.playlistVideoRenderer.lengthSeconds) - parseInt(a.playlistVideoRenderer.lengthSeconds);
                            });
                        } else if( (evnt.target.innerHTML == (' ' + ypvlmt + '↓')) || (evnt.target.innerHTML == (' ' + ypvlmt + '↑↓')) ){
                            ypvrearr = [...ypvricarr].sort((a,b)=>{
                                return parseInt(a.playlistVideoRenderer.lengthSeconds) - parseInt(b.playlistVideoRenderer.lengthSeconds);
                            });
                        };
                        function IsrtSrtSim(carr1,carr2) {
                            let cnt = 0;
                            let mxle = null;
                            let arr1 = [...carr1];
                            let arr2 = [...carr2];
                            while (!arr1.equals(arr2)){
                                mxle = arr2.reduce((a,b)=>{
                                    let al = Math.abs(arr2.indexOf(a) - arr1.indexOf(a));
                                    let bl = Math.abs(arr2.indexOf(b) - arr1.indexOf(b));
                                    return ( al > bl ? a : b );
                                });
                                if(mxle && (arr1.indexOf(mxle) !== arr2.indexOf(mxle))){
                                    arr1 = arr1.move(arr1.indexOf(mxle), arr2.indexOf(mxle));
                                    cnt++;
                                };
                            };
                            return cnt;
                        };
                        let ttcnts = IsrtSrtSim(ypvricarr,ypvrearr);
                        console.log(ttcnts);
                        orgetih = (orgetih == (' ' + ypvlmt + '↑'))?(' ' + ypvlmt + '↓'):(' ' + ypvlmt + '↑');
                        setCookie('CustomSortStatus',JSON.stringify({"BtnStr":orgetih}),null);
                        evnt.target.innerHTML = orgetih;
                        console.log(ypvrearr);
                        if(MutationObserverTimer3){
                            clearTimeout(MutationObserverTimer3);
                        };
                        MutationObserverTimer3 = setTimeout(() => {
                            if(ypvrearr.length != 0){
                                let ot = document.title, ftd = 0.5;
                                async function getSApiSidHash(SAPISID, origin) {
                                    function sha1(str) {
                                        return window.crypto.subtle
                                            .digest("SHA-1", new TextEncoder().encode(str))
                                            .then((buf) => {
                                            return Array.prototype.map
                                                .call(new Uint8Array(buf), (x) => ("00" + x.toString(16)).slice(-2))
                                                .join("")
                                        });
                                    };
                                    const TIMESTAMP_MS = Date.now();
                                    const digest = await sha1(`${TIMESTAMP_MS} ${SAPISID} ${origin}`);
                                    return `${TIMESTAMP_MS}_${digest}`;
                                };
                                async function getPosts(){
                                    let reg = /\<meta name="description" content\=\"(.+?)\"/;
                                    let ttcntst = ttcnts;
                                    let startTime = new Date(), endTime, tdavg = ftd;
                                    if(ttcntst < ypvrearr.length){
                                        while (!ypvricarr.equals(ypvrearr)){
                                            let mxle = null;
                                            mxle = ypvrearr.reduce((a,b)=>{
                                                let al = Math.abs(ypvrearr.indexOf(a) - ypvricarr.indexOf(a));
                                                let bl = Math.abs(ypvrearr.indexOf(b) - ypvricarr.indexOf(b));
                                                return ( al > bl ? a : b );
                                            });
                                            if(mxle && (ypvricarr.indexOf(mxle) !== ypvrearr.indexOf(mxle))){
                                                let ytactsjson;
                                                if(ypvricarr.indexOf(mxle) < ypvrearr.indexOf(mxle)){
                                                    ytactsjson = [{
                                                        "action": "ACTION_MOVE_VIDEO_AFTER",
                                                        "setVideoId": mxle.playlistVideoRenderer.setVideoId,
                                                        "movedSetVideoIdPredecessor": ypvricarr[ypvrearr.indexOf(mxle)].playlistVideoRenderer.setVideoId
                                                    }];
                                                } else if (ypvrearr.indexOf(mxle) === 0) {
                                                    ytactsjson = [{
                                                        "action": "ACTION_MOVE_VIDEO_AFTER",
                                                        "setVideoId": mxle.playlistVideoRenderer.setVideoId
                                                    }];
                                                } else {
                                                    ytactsjson = [{
                                                        "action": "ACTION_MOVE_VIDEO_AFTER",
                                                        "setVideoId": mxle.playlistVideoRenderer.setVideoId,
                                                        "movedSetVideoIdPredecessor": ypvricarr[ypvrearr.indexOf(mxle)-1].playlistVideoRenderer.setVideoId
                                                    }];
                                                };
                                                let nsttArr = {
                                                    'en' : ' ' + ypvlmt + ' ( Remain ' + ttcnts + ' steps . )',
                                                    'zh-TW' : ' ' + ypvlmt + ' ( 剩餘 ' + ttcnts + ' 步。 )',
                                                    'zh-CN' : ' ' + ypvlmt + ' ( 剩余 ' + ttcnts + ' 步。 )',
                                                    'ja' : ' ' + ypvlmt + ' ( 残る ' + ttcnts + ' ステップ。 )',
                                                };
                                                let nstt = nsttArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)];
                                                evnt.target.innerHTML = nstt;
                                                document.title = ot + nstt;
                                                console.log('Fetching: ',mxle);
                                                console.log('Move ' + ypvricarr.indexOf(mxle) + ' to ' + ypvrearr.indexOf(mxle));
                                                try {
                                                    let p1 = await fetch("https://www.youtube.com/youtubei/v1/browse/edit_playlist?key=" + ytcfg.data_.INNERTUBE_API_KEY + "&prettyPrint=false", {
                                                        "headers": {
                                                            "accept": "*/*",
                                                            "authorization": "SAPISIDHASH " + await getSApiSidHash(document.cookie.split("SAPISID=")[1].split("; ")[0], window.origin),
                                                            "content-type": "application/json"
                                                        },
                                                        "body": JSON.stringify({
                                                            "context": {
                                                                "client": {
                                                                    clientName: "WEB",
                                                                    clientVersion: ytcfg.data_.INNERTUBE_CLIENT_VERSION
                                                                }
                                                            },
                                                            "actions": ytactsjson,
                                                            "playlistId": "WL"
                                                        }),
                                                        "method": "POST"
                                                    });
                                                    ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
                                                    ttcnts--;
                                                } catch (err) {
                                                    console.error(err.message);
                                                };
                                                endTime = new Date();
                                                let timeDiff = endTime - startTime;
                                                tdavg = (tdavg + (timeDiff/1000)) / 2;
                                                tdavg = Math.round(tdavg*10)/10;
                                                evnt.target.style.transition = 'all ' + tdavg + 's';
                                                evnt.target.style.boxShadow = 'inset -' + evnt.target.offsetWidth*(ttcnts/ttcntst) + 'px 0px rgba(255, 255, 255, 0.2)';
                                                startTime = endTime;
                                            };
                                        };
                                    } else {
                                        ttcnts = ypvrearr.length - 1;
                                        ttcntst = ttcnts;
                                        for(let ypvrei=0;ypvrei<ypvrearr.length;ypvrei++){
                                            let mxle = ypvrearr[ypvrearr.length - ypvrei - 1];
                                            let ytactsjson = [{
                                                "action": "ACTION_MOVE_VIDEO_AFTER",
                                                "setVideoId": mxle.playlistVideoRenderer.setVideoId
                                            }];
                                            let nsttArr = {
                                                'en' : ' ' + ypvlmt + ' ( Remain ' + ttcnts + ' steps . )',
                                                'zh-TW' : ' ' + ypvlmt + ' ( 剩餘 ' + ttcnts + ' 步。 )',
                                                'zh-CN' : ' ' + ypvlmt + ' ( 剩余 ' + ttcnts + ' 步。 )',
                                                'ja' : ' ' + ypvlmt + ' ( 残る ' + ttcnts + ' ステップ。 )',
                                            };
                                            let nstt = nsttArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)];
                                            evnt.target.innerHTML = nstt;
                                            document.title = ot + nstt;
                                            console.log('Fetching: ',mxle);
                                            console.log('Move ' + ypvricarr.indexOf(mxle) + ' to ' + ypvrearr.indexOf(mxle));
                                            try {
                                                let p1 = await fetch("https://www.youtube.com/youtubei/v1/browse/edit_playlist?key=" + ytcfg.data_.INNERTUBE_API_KEY + "&prettyPrint=false", {
                                                    "headers": {
                                                        "accept": "*/*",
                                                        "authorization": "SAPISIDHASH " + await getSApiSidHash(document.cookie.split("SAPISID=")[1].split("; ")[0], window.origin),
                                                        "content-type": "application/json"
                                                    },
                                                    "body": JSON.stringify({
                                                        "context": {
                                                            "client": {
                                                                clientName: "WEB",
                                                                clientVersion: ytcfg.data_.INNERTUBE_CLIENT_VERSION
                                                            }
                                                        },
                                                        "actions": ytactsjson,
                                                        "playlistId": "WL"
                                                    }),
                                                    "method": "POST"
                                                });
                                                ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
                                                ttcnts--;
                                            } catch (err) {
                                                console.error(err.message);
                                            };
                                            endTime = new Date();
                                            let timeDiff = endTime - startTime;
                                            tdavg = (tdavg + (timeDiff/1000)) / 2;
                                            tdavg = Math.round(tdavg*10)/10;
                                            evnt.target.style.transition = 'all ' + tdavg + 's';
                                            evnt.target.style.boxShadow = 'inset -' + evnt.target.offsetWidth*(ttcnts/ttcntst) + 'px 0px rgba(255, 255, 255, 0.2)';
                                            startTime = endTime;
                                            ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
                                        };
                                    };
                                };
                                if(!ypvricarr.equals(ypvrearr)){
                                    evnt.target.style.transition = 'all ' + ftd + 's';
                                    getPosts().then(()=>{
                                        document.title = ot;
                                        evnt.target.innerHTML = orgetih;
                                        evnt.target.style = '';
                                        console.log('Done. ');
                                        window.location.href = window.location.href;
                                    });
                                };
                            };
                        },1000);
                    });
                };
            }, 500);
        });
        observer.observe(document, {attributes:true, childList:true, subtree:true});
    };
})();