使用官方API以播放时间长度排序清单
// ==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.6 // @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}); })();