YouTube 以播放时间长度排序播放清单

使用官方API以播放时间长度排序清单

当前为 2023-10-28 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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
// @homepageURL https://greasyfork.org/zh-TW/users/142344-jasn-hr
// @supportURL https://greasyfork.org/zh-TW/users/142344-jasn-hr
// @contributionURL https://greasyfork.org/zh-TW/users/142344-jasn-hr
// @version 1.1
// @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.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 oldLH = '';
        let MutationObserverTimer, observer;
        observer = new MutationObserver( (mutations) => {
            let ypvlse = null;
            if( (oldLH !== window.location.href) && (ypvlse = document.querySelector('div#icon-label')) ){
                console.log("YouTube sort playlists by play time length is loading.");
                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;
                let MutationObserverTimer3;
                let ypvlmevntfn = (evnt) => {
                    evnt.preventDefault();
                    evnt.stopPropagation();
                    evnt.stopImmediatePropagation();
                    console.log(evnt);
                    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]))));
                    if(getFndPath) {
                        let ypvricarr = [...eval('(' + getFndPath + ')')];
                        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(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 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);
                };
            };
        });
        observer.observe(document, {attributes:true, childList:true, subtree:true});
    };
})();