a29527806 / YouTube sort playlists by play time length

// ==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://greasyfork.org/zh-TW/users/142344-jasn-hr)
// @license            MIT
// @icon               https://www.google.com/s2/favicons?domain=www.youtube.com
// @homepageURL        https://greasyfork.org/zh-TW/users/142344-jasn-hr
// @supportURL         https://greasyfork.org/zh-TW/users/142344-jasn-hr
// @version            1.5
// @namespace          https://greasyfork.org/zh-TW/users/142344-jasn-hr
// @grant              none
// @match              http*://www.youtube.com/*
// @exclude            http*://www.google.com/*

// ==/UserScript==

(() => {
    console.log("YouTube sort playlists by play time length is loading.");
    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 oldLH = '';
    let observerYSPBPTL;
    observerYSPBPTL = new MutationObserver( (mutations) => {
        let ypvlse = null;
        if( (oldLH !== window.location.href) && (ypvlse = document.querySelector('div#icon-label')) ){
            oldLH = window.location.href;
            let gck = JSON.parse(getCookie('CustomSortStatus'));
            let ypvlmtArr = {
                'en':'Play time length',
                'zh-TW':'播放時長',
                'zh-CN':'播放时长',
                'ja': 'プレイ時間'
            };
            let ypvlmt = ypvlmtArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)] || ypvlmtArr.en;
            function searchObj(path, obj, target) {
                for (let k in obj) {
                    if (obj.hasOwnProperty(k)){
                        if(obj[k] === target){
                            return path + "['" + k + "']";
                        } else if (typeof obj[k] === 'object') {
                            let result = searchObj(path + "['" + k + "']", obj[k], target);
                            if (result){
                                return result;
                            };
                        };
                    };
                };
                return false;
            };
            function getObjPathParent(srcPath){
                let tgPath = null;
                if( (srcPath) && (srcPath.replace) ){tgPath = srcPath.replace(/\[[^\[\]]+\]$/,'')};
                return tgPath;
            };
            let getFndPath = getObjPathParent(getObjPathParent(getObjPathParent(searchObj("ytInitialData",ytInitialData,document.querySelector('ytd-playlist-video-list-renderer div#contents ytd-playlist-video-renderer a[href *= "/watch?v="]').href.match(/v=([^\=\&]+)&?/)[1]))));
            let ypvricarr = [];
            let MutationObserverTimerYSPBPTL3;
            let ypvlmevntfn = (evnt) => {
                evnt.preventDefault();
                evnt.stopPropagation();
                evnt.stopImmediatePropagation();
                console.log(evnt);
                getFndPath = getObjPathParent(getObjPathParent(getObjPathParent(searchObj("ytInitialData",ytInitialData,document.querySelector('ytd-playlist-video-list-renderer div#contents ytd-playlist-video-renderer a[href *= "/watch?v="]').href.match(/v=([^\=\&]+)&?/)[1]))));
                ypvricarr = [];
                try{
                    ypvricarr = [...eval('(' + getFndPath + ')')];
                }catch(err){
                    console.log(err);
                };
                if(ypvricarr.length != 0){
                    let ypvrearr = [];
                    let orgetih = evnt.target.innerHTML;
                    if(orgetih == (' ' + ypvlmt + '▲')){
                        ypvrearr = [...ypvricarr].sort((a,b)=>{
                            return parseInt(b.playlistVideoRenderer.lengthSeconds) - parseInt(a.playlistVideoRenderer.lengthSeconds);
                        });
                    } else if( (orgetih == (' ' + ypvlmt + '▼')) || (orgetih == (' ' + ypvlmt + '▲▼')) ){
                        ypvrearr = [...ypvricarr].sort((a,b)=>{
                            return parseInt(a.playlistVideoRenderer.lengthSeconds) - parseInt(b.playlistVideoRenderer.lengthSeconds);
                        });
                    } else {
                        ypvrearr = [...ypvricarr].sort((a,b)=>{
                            return parseInt(a.playlistVideoRenderer.lengthSeconds) - parseInt(b.playlistVideoRenderer.lengthSeconds);
                        });
                        orgetih = (' ' + ypvlmt + '▼');
                        evnt.target.innerHTML = orgetih;
                    };
                    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 + '▲');
                    if(gck = JSON.parse(getCookie('CustomSortStatus'))){
                        gck.BtnStr = orgetih;
                        setCookie('CustomSortStatus',JSON.stringify(gck),null);
                    } else {
                        setCookie('CustomSortStatus',JSON.stringify({"BtnStr":orgetih}),null);
                    }
                    evnt.target.innerHTML = orgetih;
                    console.log(ypvrearr);
                    if(MutationObserverTimerYSPBPTL3){
                        clearTimeout(MutationObserverTimerYSPBPTL3);
                    };
                    MutationObserverTimerYSPBPTL3 = 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 fetchYTMoveAPI(actions,playlistId){
                                return 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": actions,
                                        "playlistId": playlistId
                                    }),
                                    "method": "POST"
                                });
                            };
                            async function moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg){
                                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)] || ypvlmtArr.en;
                                evnt.target.innerHTML = nstt;
                                document.title = ot + nstt;
                                console.log('Fetching: ',mxle);
                                console.log('Move ' + ypvricarr.indexOf(mxle) + ' to ' + ypvrearr.indexOf(mxle));
                                try {
                                    await fetchYTMoveAPI(ytactsjson,oldLH.match(/\?list=([^=&\?]+)&?/)[1]);
                                    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;
                                return {"evnt" : evnt ,
                                        "ypvlmt" : ypvlmt ,
                                        "ypvricarr" : ypvricarr ,
                                        "ypvrearr" : ypvrearr ,
                                        "ttcnts" : ttcnts ,
                                        "ttcntst" : ttcntst ,
                                        "mxle" : mxle ,
                                        "ytactsjson" : ytactsjson ,
                                        "startTime" : startTime ,
                                        "endTime" : endTime ,
                                        "tdavg" : tdavg};
                            };
                            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 mytird = await moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg);
                                            evnt = mytird.evnt;
                                            ypvlmt = mytird.ypvlmt;
                                            ypvricarr = mytird.ypvricarr;
                                            ypvrearr = mytird.ypvrearr;
                                            ttcnts = mytird.ttcnts;
                                            ttcntst = mytird.ttcntst;
                                            mxle = mytird.mxle;
                                            ytactsjson = mytird.ytactsjson;
                                            startTime = mytird.startTime;
                                            endTime = mytird.endTime;
                                            tdavg = mytird.tdavg;
                                            ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
                                        };
                                    };
                                } 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 mytird = await moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg);
                                        evnt = mytird.evnt;
                                        ypvlmt = mytird.ypvlmt;
                                        ypvricarr = mytird.ypvricarr;
                                        ypvrearr = mytird.ypvrearr;
                                        ttcnts = mytird.ttcnts;
                                        ttcntst = mytird.ttcntst;
                                        mxle = mytird.mxle;
                                        ytactsjson = mytird.ytactsjson;
                                        startTime = mytird.startTime;
                                        endTime = mytird.endTime;
                                        tdavg = mytird.tdavg;
                                        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);
                } else {
                    if(gck = JSON.parse(getCookie('CustomSortStatus'))){
                        gck.LastAct = 'Sorting';
                        setCookie('CustomSortStatus',JSON.stringify(gck),null);
                    } else {
                        setCookie('CustomSortStatus',JSON.stringify({'LastAct':'Sorting'}),null);
                    };
                    window.location.href = window.location.href;
                };
            };
            let ypvlmes = [[...ypvlse.parentNode.children].find(cn => cn.innerText.match(ypvlmt))];
            console.log(ypvlmes);
            if( (ypvlmes) && (ypvlmes.length !== 0) ){
                ypvlmes.forEach((ypvlme)=>{
                    if(ypvlme){
                        ypvlme.removeEventListener('click',ypvlmevntfn);
                        ypvlme.remove();
                    };
                });
            };
            let yvlmen = '';
            if( !(gck = JSON.parse(getCookie('CustomSortStatus'))) ){
                yvlmen = ypvlmt + '▲▼';
            } else {
                yvlmen = gck.BtnStr;
            };
            let ypvlme = ypvlse.cloneNode(true);
            ypvlme.innerHTML = yvlmen;
            ypvlse.parentNode.insertBefore(ypvlme,ypvlse.nextSibling);
            ypvlme.addEventListener('click',ypvlmevntfn);
            if(gck = JSON.parse(getCookie('CustomSortStatus'))){
                if(gck.LastAct == 'Sorting'){
                    gck.LastAct = 'Nothing';
                    setCookie('CustomSortStatus',JSON.stringify(gck),null);
                    //ypvlme.click();
                };
            } else {
                setCookie('CustomSortStatus',JSON.stringify({'LastAct':'Nothing'}),null);
            };
            try{
                ypvricarr = [...eval('(' + getFndPath + ')')];
            }catch(err){
                let nfrstrArr = {
                    'en':'click to fresh page one time.',
                    'zh-TW':'按下以重新整理一次',
                    'zh-CN':'按下以重新整理一次',
                    'ja': '押してページを 1 回更新します'
                };
                let nfrst = nfrstrArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)] || nfrstrArr.en;
                ypvlme.innerHTML = (' ' + ypvlmt + ' ( ' + nfrst +' ) ');
                console.log(err);
            };
            console.log("YouTube sort playlists by play time length is loaded.");
        };
    });
    observerYSPBPTL.observe(document, {attributes:true, childList:true, subtree:true});
})();