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

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

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

  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 MIT
  13. // @icon
  14. // @homepageURL https://greasyfork.org/zh-TW/users/142344-jasn-hr
  15. // @supportURL https://greasyfork.org/zh-TW/users/142344-jasn-hr
  16. // @contributionURL https://greasyfork.org/zh-TW/users/142344-jasn-hr
  17. // @version 1.0
  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. 'use strict';
  27. window.onload = () => {
  28. let setCookie = (name,value,days) => {
  29. let expires = "";
  30. if (days) {
  31. let date = new Date();
  32. date.setTime(date.getTime() + (days*24*60*60*1000));
  33. expires = "; expires=" + date.toUTCString();
  34. }
  35. document.cookie = name + "=" + (value || "") + expires + "; path=/";
  36. };
  37. let getCookie=(name) =>{
  38. let nameEQ = name + "=";
  39. let ca = document.cookie.split(';');
  40. for(let i=0;i < ca.length;i++) {
  41. let c = ca[i];
  42. while (c.charAt(0)==' ') c = c.substring(1,c.length);
  43. if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
  44. }
  45. return null;
  46. };
  47. let eraseCookie=(name) =>{
  48. document.cookie = name +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
  49. };
  50. if (typeof Array.prototype.equals === "undefined") {
  51. Array.prototype.equals = function( array ) {
  52. return this.length == array.length &&
  53. this.every( function(this_i,i) { return this_i == array[i] } )
  54. };
  55. };
  56. if (typeof Array.prototype.move === "undefined") {
  57. Array.prototype.move = function(from, to, on = 1) {
  58. return this.splice(to, 0, ...this.splice(from, on)), this
  59. };
  60. };
  61. let oldLH = '';
  62. let MutationObserverTimer, observer;
  63. observer = new MutationObserver( (mutations) => {
  64. let ypvlse = null;
  65. if( (oldLH !== window.location.href) && (ypvlse = document.querySelector('div#icon-label')) ){
  66. console.log("YouTube sort playlists by play time length is loading.");
  67. oldLH = window.location.href;
  68. let ypvlmtArr = {
  69. 'en':'Play time length',
  70. 'zh-TW':'播放時長',
  71. 'zh-CN':'播放时长',
  72. 'ja': 'プレイ時間'
  73. };
  74. let ypvlmt = ypvlmtArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)] || ypvlmtArr.en;
  75. if(![...ypvlse.parentNode.children].find(cn => cn.innerText.match(ypvlmt))){
  76. let yvlmen = '';
  77. if(!(getCookie('CustomSortStatus'))){
  78. yvlmen = ypvlmt + '▲▼';
  79. } else {
  80. yvlmen = JSON.parse(getCookie('CustomSortStatus')).BtnStr;
  81. };
  82. const ypvlme = ypvlse.cloneNode(true);
  83. ypvlme.innerHTML = yvlmen;
  84. ypvlse.parentNode.insertBefore(ypvlme,ypvlse.nextSibling);
  85. let MutationObserverTimer3;
  86. ypvlme.addEventListener('click',(evnt) => {
  87. observer.disconnect();
  88. evnt.preventDefault();
  89. evnt.stopPropagation();
  90. evnt.stopImmediatePropagation();
  91. function searchObj(path, obj, target) {
  92. for (let k in obj) {
  93. if (obj.hasOwnProperty(k)){
  94. if(obj[k] === target){
  95. return path + "['" + k + "']";
  96. } else if (typeof obj[k] === 'object') {
  97. let result = searchObj(path + "['" + k + "']", obj[k], target);
  98. if (result){
  99. return result;
  100. };
  101. };
  102. };
  103. };
  104. return false;
  105. };
  106. function getObjPathParent(srcPath){
  107. let tgPath = null;
  108. if( (srcPath) && (srcPath.replace) ){tgPath = srcPath.replace(/\[[^\[\]]+\]$/,'')};
  109. return tgPath;
  110. };
  111. 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]))));
  112. if(getFndPath) {
  113. let ypvricarr = [...eval('(' + getFndPath + ')')];
  114. let ypvrearr = [];
  115. let orgetih = evnt.target.innerHTML;
  116. if(orgetih == (' ' + ypvlmt + '▲')){
  117. ypvrearr = [...ypvricarr].sort((a,b)=>{
  118. return parseInt(b.playlistVideoRenderer.lengthSeconds) - parseInt(a.playlistVideoRenderer.lengthSeconds);
  119. });
  120. } else if( (orgetih == (' ' + ypvlmt + '▼')) || (orgetih == (' ' + ypvlmt + '▲▼')) ){
  121. ypvrearr = [...ypvricarr].sort((a,b)=>{
  122. return parseInt(a.playlistVideoRenderer.lengthSeconds) - parseInt(b.playlistVideoRenderer.lengthSeconds);
  123. });
  124. } else {
  125. ypvrearr = [...ypvricarr].sort((a,b)=>{
  126. return parseInt(a.playlistVideoRenderer.lengthSeconds) - parseInt(b.playlistVideoRenderer.lengthSeconds);
  127. });
  128. orgetih = (' ' + ypvlmt + '▼');
  129. evnt.target.innerHTML = orgetih;
  130. };
  131. function IsrtSrtSim(carr1,carr2) {
  132. let cnt = 0;
  133. let mxle = null;
  134. let arr1 = [...carr1];
  135. let arr2 = [...carr2];
  136. while (!arr1.equals(arr2)){
  137. mxle = arr2.reduce((a,b)=>{
  138. let al = Math.abs(arr2.indexOf(a) - arr1.indexOf(a));
  139. let bl = Math.abs(arr2.indexOf(b) - arr1.indexOf(b));
  140. return ( al > bl ? a : b );
  141. });
  142. if(mxle && (arr1.indexOf(mxle) !== arr2.indexOf(mxle))){
  143. arr1 = arr1.move(arr1.indexOf(mxle), arr2.indexOf(mxle));
  144. cnt++;
  145. };
  146. };
  147. return cnt;
  148. };
  149. let ttcnts = IsrtSrtSim(ypvricarr,ypvrearr);
  150. console.log(ttcnts);
  151. orgetih = (orgetih == (' ' + ypvlmt + '▲'))?(' ' + ypvlmt + '▼'):(' ' + ypvlmt + '▲');
  152. setCookie('CustomSortStatus',JSON.stringify({"BtnStr":orgetih}),null);
  153. evnt.target.innerHTML = orgetih;
  154. console.log(ypvrearr);
  155. if(MutationObserverTimer3){
  156. clearTimeout(MutationObserverTimer3);
  157. };
  158. MutationObserverTimer3 = setTimeout(() => {
  159. if(ypvrearr.length != 0){
  160. let ot = document.title, ftd = 0.5;
  161. async function getSApiSidHash(SAPISID, origin) {
  162. function sha1(str) {
  163. return window.crypto.subtle
  164. .digest("SHA-1", new TextEncoder().encode(str))
  165. .then((buf) => {
  166. return Array.prototype.map
  167. .call(new Uint8Array(buf), (x) => ("00" + x.toString(16)).slice(-2))
  168. .join("")
  169. });
  170. };
  171. const TIMESTAMP_MS = Date.now();
  172. const digest = await sha1(`${TIMESTAMP_MS} ${SAPISID} ${origin}`);
  173. return `${TIMESTAMP_MS}_${digest}`;
  174. };
  175. async function fetchYTMoveAPI(actions,playlistId){
  176. return fetch("https://www.youtube.com/youtubei/v1/browse/edit_playlist?key=" + ytcfg.data_.INNERTUBE_API_KEY + "&prettyPrint=false", {
  177. "headers": {
  178. "accept": "*/*",
  179. "authorization": "SAPISIDHASH " + await getSApiSidHash(document.cookie.split("SAPISID=")[1].split("; ")[0], window.origin),
  180. "content-type": "application/json"
  181. },
  182. "body": JSON.stringify({
  183. "context": {
  184. "client": {
  185. clientName: "WEB",
  186. clientVersion: ytcfg.data_.INNERTUBE_CLIENT_VERSION
  187. }
  188. },
  189. "actions": actions,
  190. "playlistId": playlistId
  191. }),
  192. "method": "POST"
  193. });
  194. };
  195. async function moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg){
  196. let nsttArr = {
  197. 'en' : ' ' + ypvlmt + ' ( Remain ' + ttcnts + ' steps . )',
  198. 'zh-TW' : ' ' + ypvlmt + ' ( 剩餘 ' + ttcnts + ' 步。 )',
  199. 'zh-CN' : ' ' + ypvlmt + ' ( 剩余 ' + ttcnts + ' 步。 )',
  200. 'ja' : ' ' + ypvlmt + ' ( 残る ' + ttcnts + ' ステップ。 )',
  201. };
  202. let nstt = nsttArr[(navigator.userLanguage || navigator.language || navigator.browserLanguage || navigator.systemLanguage)] || ypvlmtArr['en'];
  203. evnt.target.innerHTML = nstt;
  204. document.title = ot + nstt;
  205. console.log('Fetching: ',mxle);
  206. console.log('Move ' + ypvricarr.indexOf(mxle) + ' to ' + ypvrearr.indexOf(mxle));
  207. try {
  208. await fetchYTMoveAPI(ytactsjson,oldLH.match(/\?list=([^=&\?]+)&?/)[1]);
  209. ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
  210. ttcnts--;
  211. } catch (err) {
  212. console.error(err.message);
  213. };
  214. endTime = new Date();
  215. let timeDiff = endTime - startTime;
  216. tdavg = (tdavg + (timeDiff/1000)) / 2;
  217. tdavg = Math.round(tdavg*10)/10;
  218. evnt.target.style.transition = 'all ' + tdavg + 's';
  219. evnt.target.style.boxShadow = 'inset -' + evnt.target.offsetWidth*(ttcnts/ttcntst) + 'px 0px rgba(255, 255, 255, 0.2)';
  220. startTime = endTime;
  221. return {"evnt" : evnt ,
  222. "ypvlmt" : ypvlmt ,
  223. "ypvricarr" : ypvricarr ,
  224. "ypvrearr" : ypvrearr ,
  225. "ttcnts" : ttcnts ,
  226. "ttcntst" : ttcntst ,
  227. "mxle" : mxle ,
  228. "ytactsjson" : ytactsjson ,
  229. "startTime" : startTime ,
  230. "endTime" : endTime ,
  231. "tdavg" : tdavg};
  232. };
  233. async function getPosts(){
  234. let reg = /\<meta name="description" content\=\"(.+?)\"/;
  235. let ttcntst = ttcnts;
  236. let startTime = new Date(), endTime, tdavg = ftd;
  237. if(ttcntst < ypvrearr.length){
  238. while (!ypvricarr.equals(ypvrearr)){
  239. let mxle = null;
  240. mxle = ypvrearr.reduce((a,b)=>{
  241. let al = Math.abs(ypvrearr.indexOf(a) - ypvricarr.indexOf(a));
  242. let bl = Math.abs(ypvrearr.indexOf(b) - ypvricarr.indexOf(b));
  243. return ( al > bl ? a : b );
  244. });
  245. if(mxle && (ypvricarr.indexOf(mxle) !== ypvrearr.indexOf(mxle))){
  246. let ytactsjson;
  247. if(ypvricarr.indexOf(mxle) < ypvrearr.indexOf(mxle)){
  248. ytactsjson = [{
  249. "action": "ACTION_MOVE_VIDEO_AFTER",
  250. "setVideoId": mxle.playlistVideoRenderer.setVideoId,
  251. "movedSetVideoIdPredecessor": ypvricarr[ypvrearr.indexOf(mxle)].playlistVideoRenderer.setVideoId
  252. }];
  253. } else if (ypvrearr.indexOf(mxle) === 0) {
  254. ytactsjson = [{
  255. "action": "ACTION_MOVE_VIDEO_AFTER",
  256. "setVideoId": mxle.playlistVideoRenderer.setVideoId
  257. }];
  258. } else {
  259. ytactsjson = [{
  260. "action": "ACTION_MOVE_VIDEO_AFTER",
  261. "setVideoId": mxle.playlistVideoRenderer.setVideoId,
  262. "movedSetVideoIdPredecessor": ypvricarr[ypvrearr.indexOf(mxle)-1].playlistVideoRenderer.setVideoId
  263. }];
  264. };
  265. let mytird = await moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg);
  266. evnt = mytird.evnt;
  267. ypvlmt = mytird.ypvlmt;
  268. ypvricarr = mytird.ypvricarr;
  269. ypvrearr = mytird.ypvrearr;
  270. ttcnts = mytird.ttcnts;
  271. ttcntst = mytird.ttcntst;
  272. mxle = mytird.mxle;
  273. ytactsjson = mytird.ytactsjson;
  274. startTime = mytird.startTime;
  275. endTime = mytird.endTime;
  276. tdavg = mytird.tdavg;
  277. ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
  278. };
  279. };
  280. } else {
  281. ttcnts = ypvrearr.length - 1;
  282. ttcntst = ttcnts;
  283. for(let ypvrei=0;ypvrei<ypvrearr.length;ypvrei++){
  284. let mxle = ypvrearr[ypvrearr.length - ypvrei - 1];
  285. let ytactsjson = [{
  286. "action": "ACTION_MOVE_VIDEO_AFTER",
  287. "setVideoId": mxle.playlistVideoRenderer.setVideoId
  288. }];
  289. let nsttArr = {
  290. 'en' : ' ' + ypvlmt + ' ( Remain ' + ttcnts + ' steps . )',
  291. 'zh-TW' : ' ' + ypvlmt + ' ( 剩餘 ' + ttcnts + ' 步。 )',
  292. 'zh-CN' : ' ' + ypvlmt + ' ( 剩余 ' + ttcnts + ' 步。 )',
  293. 'ja' : ' ' + ypvlmt + ' ( 残る ' + ttcnts + ' ステップ。 )',
  294. };let mytird = await moveYTItem(evnt,ypvlmt,ypvricarr,ypvrearr,ttcnts,ttcntst,mxle,ytactsjson,startTime,endTime,tdavg);
  295. evnt = mytird.evnt;
  296. ypvlmt = mytird.ypvlmt;
  297. ypvricarr = mytird.ypvricarr;
  298. ypvrearr = mytird.ypvrearr;
  299. ttcnts = mytird.ttcnts;
  300. ttcntst = mytird.ttcntst;
  301. mxle = mytird.mxle;
  302. ytactsjson = mytird.ytactsjson;
  303. startTime = mytird.startTime;
  304. endTime = mytird.endTime;
  305. tdavg = mytird.tdavg;
  306. ypvricarr = ypvricarr.move(ypvricarr.indexOf(mxle), ypvrearr.indexOf(mxle));
  307. };
  308. };
  309. };
  310. if(!ypvricarr.equals(ypvrearr)){
  311. evnt.target.style.transition = 'all ' + ftd + 's';
  312. getPosts().then(()=>{
  313. document.title = ot;
  314. evnt.target.innerHTML = orgetih;
  315. evnt.target.style = '';
  316. console.log('Done. ');
  317. window.location.href = window.location.href;
  318. });
  319. };
  320. };
  321. },1000);
  322. };
  323. });
  324. };
  325. };
  326. });
  327. observer.observe(document, {attributes:true, childList:true, subtree:true});
  328. };
  329. })();