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

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

  1. // ==UserScript==
  2.  
  3. // @name YouTube sort playlists by play time length
  4. // @name:zh-TW YouTube 以播放時間長度排序播放清單
  5. // @name:zh-CN YouTube 以播放时间长度排序播放清单
  6. // @name:ja YouTube でプレイリストを再生時間順に並べ替える
  7. // @description Sorting playlists by play time length use internal API .
  8. // @description:zh-TW 使用官方API以播放時間長度排序清單
  9. // @description:zh-CN 使用官方API以播放时间长度排序清单
  10. // @description:ja 再生時間の長さによるプレイリストの並べ替えには、内部 API を使用します。
  11. // @copyright 2023, HrJasn (https://greasyfork.org/zh-TW/users/142344-jasn-hr)
  12. // @license GPL3
  13. // @license Copyright HrJasn
  14. // @icon https://www.google.com/s2/favicons?domain=www.youtube.com
  15. // @homepageURL https://greasyfork.org/zh-TW/users/142344-jasn-hr
  16. // @supportURL https://greasyfork.org/zh-TW/users/142344-jasn-hr
  17. // @version 1.7
  18. // @namespace https://greasyfork.org/zh-TW/users/142344-jasn-hr
  19. // @grant none
  20. // @match http*://www.youtube.com/*
  21. // @exclude http*://www.google.com/*
  22.  
  23. // ==/UserScript==
  24.  
  25. (() => {
  26. console.log("YouTube sort playlists by play time length is loading.");
  27. let setCookie = (name,value,days) => {
  28. let expires = "";
  29. if (days) {
  30. let date = new Date();
  31. date.setTime(date.getTime() + (days*24*60*60*1000));
  32. expires = "; expires=" + date.toUTCString();
  33. }
  34. document.cookie = name + "=" + (value || "") + expires + "; path=/";
  35. };
  36. let getCookie=(name) =>{
  37. let nameEQ = name + "=";
  38. let ca = document.cookie.split(';');
  39. for(let i=0;i < ca.length;i++) {
  40. let c = ca[i];
  41. while (c.charAt(0)==' ') c = c.substring(1,c.length);
  42. if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
  43. }
  44. return null;
  45. };
  46. let eraseCookie=(name) =>{
  47. document.cookie = name +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
  48. };
  49. if (typeof Array.prototype.equals === "undefined") {
  50. Array.prototype.equals = function( array ) {
  51. return this.length == array.length &&
  52. this.every( function(this_i,i) { return this_i == array[i] } )
  53. };
  54. };
  55. if (typeof Array.prototype.move === "undefined") {
  56. Array.prototype.move = function(from, to, on = 1) {
  57. return this.splice(to, 0, ...this.splice(from, on)), this
  58. };
  59. };
  60. let oldLH = '';
  61. let observerYSPBPTL;
  62. observerYSPBPTL = new MutationObserver( (mutations) => {
  63. let ypvlse = null;
  64. if( (oldLH !== window.location.href) && (ypvlse = document.querySelector('div#icon-label')) ){
  65. oldLH = window.location.href;
  66. let gck = JSON.parse(getCookie('CustomSortStatus'));
  67. let ypvlmtArr = {
  68. 'en':'Play time length',
  69. 'zh-TW':'播放時長',
  70. 'zh-CN':'播放时长',
  71. 'ja': 'プレイ時間'
  72. };
  73. let ypvlmt = ypvlmtArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)] || ypvlmtArr.en;
  74. function searchObj(path, obj, target) {
  75. for (let k in obj) {
  76. if (obj.hasOwnProperty(k)){
  77. if(obj[k] === target){
  78. return path + "['" + k + "']";
  79. } else if (typeof obj[k] === 'object') {
  80. let result = searchObj(path + "['" + k + "']", obj[k], target);
  81. if (result){
  82. return result;
  83. };
  84. };
  85. };
  86. };
  87. return false;
  88. };
  89. function getObjPathParent(srcPath){
  90. let tgPath = null;
  91. if( (srcPath) && (srcPath.replace) ){tgPath = srcPath.replace(/\[[^\[\]]+\]$/,'')};
  92. return tgPath;
  93. };
  94. 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]))));
  95. let ypvricarr = [];
  96. let MutationObserverTimerYSPBPTL3;
  97. let ypvlmevntfn = (evnt) => {
  98. evnt.preventDefault();
  99. evnt.stopPropagation();
  100. evnt.stopImmediatePropagation();
  101. console.log(evnt);
  102. 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]))));
  103. ypvricarr = [];
  104. try{
  105. ypvricarr = [...eval('(' + getFndPath + ')')];
  106. }catch(err){
  107. console.log(err);
  108. };
  109. if(ypvricarr.length != 0){
  110. let ypvrearr = [];
  111. let orgetih = evnt.target.innerHTML;
  112. if(orgetih == (' ' + ypvlmt + '↑')){
  113. ypvrearr = [...ypvricarr].sort((a,b)=>{
  114. return parseInt(b.playlistVideoRenderer.lengthSeconds) - parseInt(a.playlistVideoRenderer.lengthSeconds);
  115. });
  116. } else if( (orgetih == (' ' + ypvlmt + '↓')) || (orgetih == (' ' + ypvlmt + '↑↓')) ){
  117. ypvrearr = [...ypvricarr].sort((a,b)=>{
  118. return parseInt(a.playlistVideoRenderer.lengthSeconds) - parseInt(b.playlistVideoRenderer.lengthSeconds);
  119. });
  120. } else {
  121. ypvrearr = [...ypvricarr].sort((a,b)=>{
  122. return parseInt(a.playlistVideoRenderer.lengthSeconds) - parseInt(b.playlistVideoRenderer.lengthSeconds);
  123. });
  124. orgetih = (' ' + ypvlmt + '↓');
  125. evnt.target.innerHTML = orgetih;
  126. };
  127. function IsrtSrtSim(carr1,carr2) {
  128. let cnt = 0;
  129. let mxle = null;
  130. let arr1 = [...carr1];
  131. let arr2 = [...carr2];
  132. while (!arr1.equals(arr2)){
  133. mxle = arr2.reduce((a,b)=>{
  134. let al = Math.abs(arr2.indexOf(a) - arr1.indexOf(a));
  135. let bl = Math.abs(arr2.indexOf(b) - arr1.indexOf(b));
  136. return ( al > bl ? a : b );
  137. });
  138. if(mxle && (arr1.indexOf(mxle) !== arr2.indexOf(mxle))){
  139. arr1 = arr1.move(arr1.indexOf(mxle), arr2.indexOf(mxle));
  140. cnt++;
  141. };
  142. };
  143. return cnt;
  144. };
  145. let ttcnts = IsrtSrtSim(ypvricarr,ypvrearr);
  146. console.log(ttcnts);
  147. orgetih = (orgetih == (' ' + ypvlmt + '↑'))?(' ' + ypvlmt + '↓'):(' ' + ypvlmt + '↑');
  148. if(gck = JSON.parse(getCookie('CustomSortStatus'))){
  149. gck.BtnStr = orgetih;
  150. setCookie('CustomSortStatus',JSON.stringify(gck),null);
  151. } else {
  152. setCookie('CustomSortStatus',JSON.stringify({"BtnStr":orgetih}),null);
  153. }
  154. evnt.target.innerHTML = orgetih;
  155. console.log(ypvrearr);
  156. if(MutationObserverTimerYSPBPTL3){
  157. clearTimeout(MutationObserverTimerYSPBPTL3);
  158. };
  159. MutationObserverTimerYSPBPTL3 = setTimeout(() => {
  160. if(ypvrearr.length != 0){
  161. let ot = document.title, ftd = 0.5;
  162. async function getSApiSidHash(SAPISID, origin) {
  163. function sha1(str) {
  164. return window.crypto.subtle
  165. .digest("SHA-1", new TextEncoder().encode(str))
  166. .then((buf) => {
  167. return Array.prototype.map
  168. .call(new Uint8Array(buf), (x) => ("00" + x.toString(16)).slice(-2))
  169. .join("")
  170. });
  171. };
  172. const TIMESTAMP_MS = Date.now();
  173. const digest = await sha1(`${TIMESTAMP_MS} ${SAPISID} ${origin}`);
  174. return `${TIMESTAMP_MS}_${digest}`;
  175. };
  176. async function fetchYTMoveAPI(actions,playlistId){
  177. return fetch("https://www.youtube.com/youtubei/v1/browse/edit_playlist?key=" + ytcfg.data_.INNERTUBE_API_KEY + "&prettyPrint=false", {
  178. "headers": {
  179. "accept": "*/*",
  180. "authorization": "SAPISIDHASH " + await getSApiSidHash(document.cookie.split("SAPISID=")[1].split("; ")[0], window.origin),
  181. "content-type": "application/json"
  182. },
  183. "body": JSON.stringify({
  184. "context": {
  185. "client": {
  186. clientName: "WEB",
  187. clientVersion: ytcfg.data_.INNERTUBE_CLIENT_VERSION
  188. }
  189. },
  190. "actions": actions,
  191. "playlistId": playlistId
  192. }),
  193. "method": "POST"
  194. });
  195. };
  196. async function moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg){
  197. let nsttArr = {
  198. 'en' : ' ' + ypvlmt + ' ( Remain ' + ttcnts + ' steps . )',
  199. 'zh-TW' : ' ' + ypvlmt + ' ( 剩餘 ' + ttcnts + ' 步。 )',
  200. 'zh-CN' : ' ' + ypvlmt + ' ( 剩余 ' + ttcnts + ' 步。 )',
  201. 'ja' : ' ' + ypvlmt + ' ( 残る ' + ttcnts + ' ステップ。 )',
  202. };
  203. let nstt = nsttArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)] || ypvlmtArr.en;
  204. evnt.target.innerHTML = nstt;
  205. document.title = ot + nstt;
  206. console.log('Fetching: ',mxle);
  207. console.log('Move ' + ypvricarr.indexOf(mxle) + ' to ' + ypvrearr.indexOf(mxle));
  208. try {
  209. await fetchYTMoveAPI(ytactsjson,oldLH.match(/\?list=([^=&\?]+)&?/)[1]);
  210. ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
  211. ttcnts--;
  212. } catch (err) {
  213. console.error(err.message);
  214. };
  215. endTime = new Date();
  216. let timeDiff = endTime - startTime;
  217. tdavg = (tdavg + (timeDiff/1000)) / 2;
  218. tdavg = Math.round(tdavg*10)/10;
  219. evnt.target.style.transition = 'all ' + tdavg + 's';
  220. evnt.target.style.boxShadow = 'inset -' + evnt.target.offsetWidth*(ttcnts/ttcntst) + 'px 0px rgba(255, 255, 255, 0.2)';
  221. startTime = endTime;
  222. return {"evnt" : evnt ,
  223. "ypvlmt" : ypvlmt ,
  224. "ypvricarr" : ypvricarr ,
  225. "ypvrearr" : ypvrearr ,
  226. "ttcnts" : ttcnts ,
  227. "ttcntst" : ttcntst ,
  228. "mxle" : mxle ,
  229. "ytactsjson" : ytactsjson ,
  230. "startTime" : startTime ,
  231. "endTime" : endTime ,
  232. "tdavg" : tdavg};
  233. };
  234. async function getPosts(){
  235. let reg = /\<meta name="description" content\=\"(.+?)\"/;
  236. let ttcntst = ttcnts;
  237. let startTime = new Date(), endTime, tdavg = ftd;
  238. if(ttcntst < ypvrearr.length){
  239. while (!ypvricarr.equals(ypvrearr)){
  240. let mxle = null;
  241. mxle = ypvrearr.reduce((a,b)=>{
  242. let al = Math.abs(ypvrearr.indexOf(a) - ypvricarr.indexOf(a));
  243. let bl = Math.abs(ypvrearr.indexOf(b) - ypvricarr.indexOf(b));
  244. return ( al > bl ? a : b );
  245. });
  246. if(mxle && (ypvricarr.indexOf(mxle) !== ypvrearr.indexOf(mxle))){
  247. let ytactsjson;
  248. if(ypvricarr.indexOf(mxle) < ypvrearr.indexOf(mxle)){
  249. ytactsjson = [{
  250. "action": "ACTION_MOVE_VIDEO_AFTER",
  251. "setVideoId": mxle.playlistVideoRenderer.setVideoId,
  252. "movedSetVideoIdPredecessor": ypvricarr[ypvrearr.indexOf(mxle)].playlistVideoRenderer.setVideoId
  253. }];
  254. } else if (ypvrearr.indexOf(mxle) === 0) {
  255. ytactsjson = [{
  256. "action": "ACTION_MOVE_VIDEO_AFTER",
  257. "setVideoId": mxle.playlistVideoRenderer.setVideoId
  258. }];
  259. } else {
  260. ytactsjson = [{
  261. "action": "ACTION_MOVE_VIDEO_AFTER",
  262. "setVideoId": mxle.playlistVideoRenderer.setVideoId,
  263. "movedSetVideoIdPredecessor": ypvricarr[ypvrearr.indexOf(mxle)-1].playlistVideoRenderer.setVideoId
  264. }];
  265. };
  266. let mytird = await moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg);
  267. evnt = mytird.evnt;
  268. ypvlmt = mytird.ypvlmt;
  269. ypvricarr = mytird.ypvricarr;
  270. ypvrearr = mytird.ypvrearr;
  271. ttcnts = mytird.ttcnts;
  272. ttcntst = mytird.ttcntst;
  273. mxle = mytird.mxle;
  274. ytactsjson = mytird.ytactsjson;
  275. startTime = mytird.startTime;
  276. endTime = mytird.endTime;
  277. tdavg = mytird.tdavg;
  278. ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
  279. };
  280. };
  281. } else {
  282. ttcnts = ypvrearr.length - 1;
  283. ttcntst = ttcnts;
  284. for(let ypvrei=0;ypvrei<ypvrearr.length;ypvrei++){
  285. let mxle = ypvrearr[ypvrearr.length - ypvrei - 1];
  286. let ytactsjson = [{
  287. "action": "ACTION_MOVE_VIDEO_AFTER",
  288. "setVideoId": mxle.playlistVideoRenderer.setVideoId
  289. }];
  290. let nsttArr = {
  291. 'en' : ' ' + ypvlmt + ' ( Remain ' + ttcnts + ' steps . )',
  292. 'zh-TW' : ' ' + ypvlmt + ' ( 剩餘 ' + ttcnts + ' 步。 )',
  293. 'zh-CN' : ' ' + ypvlmt + ' ( 剩余 ' + ttcnts + ' 步。 )',
  294. 'ja' : ' ' + ypvlmt + ' ( 残る ' + ttcnts + ' ステップ。 )',
  295. };let mytird = await moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg);
  296. evnt = mytird.evnt;
  297. ypvlmt = mytird.ypvlmt;
  298. ypvricarr = mytird.ypvricarr;
  299. ypvrearr = mytird.ypvrearr;
  300. ttcnts = mytird.ttcnts;
  301. ttcntst = mytird.ttcntst;
  302. mxle = mytird.mxle;
  303. ytactsjson = mytird.ytactsjson;
  304. startTime = mytird.startTime;
  305. endTime = mytird.endTime;
  306. tdavg = mytird.tdavg;
  307. ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
  308. };
  309. };
  310. };
  311. if(!ypvricarr.equals(ypvrearr)){
  312. evnt.target.style.transition = 'all ' + ftd + 's';
  313. getPosts().then(()=>{
  314. document.title = ot;
  315. evnt.target.innerHTML = orgetih;
  316. evnt.target.style = '';
  317. console.log('Done. ');
  318. window.location.href = window.location.href;
  319. });
  320. };
  321. };
  322. },1000);
  323. } else {
  324. if(gck = JSON.parse(getCookie('CustomSortStatus'))){
  325. gck.LastAct = 'Sorting';
  326. setCookie('CustomSortStatus',JSON.stringify(gck),null);
  327. } else {
  328. setCookie('CustomSortStatus',JSON.stringify({'LastAct':'Sorting'}),null);
  329. };
  330. window.location.href = window.location.href;
  331. };
  332. };
  333. let ypvlmes = [[...ypvlse.parentNode.children].find(cn => cn.innerText.match(ypvlmt))];
  334. console.log(ypvlmes);
  335. if( (ypvlmes) && (ypvlmes.length !== 0) ){
  336. ypvlmes.forEach((ypvlme)=>{
  337. if(ypvlme){
  338. ypvlme.removeEventListener('click',ypvlmevntfn);
  339. ypvlme.remove();
  340. };
  341. });
  342. };
  343. let yvlmen = '';
  344. if( !(gck = JSON.parse(getCookie('CustomSortStatus'))) ){
  345. yvlmen = ypvlmt + '↑↓';
  346. } else {
  347. yvlmen = gck.BtnStr;
  348. };
  349. let ypvlme = ypvlse.cloneNode(true);
  350. ypvlme.innerHTML = yvlmen;
  351. ypvlse.parentNode.insertBefore(ypvlme,ypvlse.nextSibling);
  352. ypvlme.addEventListener('click',ypvlmevntfn);
  353. if(gck = JSON.parse(getCookie('CustomSortStatus'))){
  354. if(gck.LastAct == 'Sorting'){
  355. gck.LastAct = 'Nothing';
  356. setCookie('CustomSortStatus',JSON.stringify(gck),null);
  357. //ypvlme.click();
  358. };
  359. } else {
  360. setCookie('CustomSortStatus',JSON.stringify({'LastAct':'Nothing'}),null);
  361. };
  362. try{
  363. ypvricarr = [...eval('(' + getFndPath + ')')];
  364. }catch(err){
  365. let nfrstrArr = {
  366. 'en':'click to fresh page one time.',
  367. 'zh-TW':'按下以重新整理一次',
  368. 'zh-CN':'按下以重新整理一次',
  369. 'ja': '押してページを 1 回更新します'
  370. };
  371. let nfrst = nfrstrArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)] || nfrstrArr.en;
  372. ypvlme.innerHTML = (' ' + ypvlmt + ' ( ' + nfrst +' ) ');
  373. console.log(err);
  374. };
  375. console.log("YouTube sort playlists by play time length is loaded.");
  376. };
  377. });
  378. observerYSPBPTL.observe(document, {attributes:true, childList:true, subtree:true});
  379. })();