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

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

目前為 2023-11-01 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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.4
// @namespace          https://greasyfork.org/zh-TW/users/142344-jasn-hr
// @grant              none
// @match              http*://www.youtube.com/*
// @exclude            http*://www.google.com/*

// ==/UserScript==

(() => {
    'use strict';
    window.addEventListener('load',() => {
        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 MutationObserverTimerYSPBPTL, 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});
    });
})();