Sort Youtube Watch Later by Duration

As the name implies, sorts youtube watch later by duration

目前為 2022-11-25 提交的版本,檢視 最新版本

  1. // Changelog 25/11:
  2. // Youtube interface change broke the button, now fixed
  3.  
  4. // Changelog 24/6:
  5. // Autoscroll delay now is not correlated with number of items in playlist
  6. // Autoscroll now triggers from the start
  7. // Added feedback to buttons
  8.  
  9. /* jshint esversion: 8 */
  10. // ==UserScript==
  11. // @name Sort Youtube Watch Later by Duration
  12. // @namespace https://gist.github.com/KohGeek/65ad9e0118ee5f5ee484676731bcd092
  13. // @version 1.0.5
  14. // @description As the name implies, sorts youtube watch later by duration
  15. // @author KohGeek
  16. // @license GNU GPLv2
  17. // @match http://*.youtube.com/playlist*
  18. // @match https://*.youtube.com/playlist*
  19. // @require https://gf.qytechs.cn/scripts/374849-library-onelementready-es7/code/Library%20%7C%20onElementReady%20ES7.js
  20. // @grant none
  21. // @run-at document-start
  22. // ==/UserScript==
  23.  
  24. // Heavily borrowed from many places
  25. // function for triggering mouse events
  26. let fireMouseEvent = (type, elem, centerX, centerY) => {
  27. var evt = document.createEvent("MouseEvents");
  28. evt.initMouseEvent(type, true, true, window, 1, 1, 1, centerX, centerY, false, false, false, false, 0, elem);
  29. elem.dispatchEvent(evt);
  30. };
  31.  
  32. // https://ghostinspector.com/blog/simulate-drag-and-drop-javascript-casperjs/
  33. let simulateDrag = (elemDrag, elemDrop) => {
  34. // calculate positions
  35. var pos = elemDrag.getBoundingClientRect();
  36. var center1X = Math.floor((pos.left + pos.right) / 2);
  37. var center1Y = Math.floor((pos.top + pos.bottom) / 2);
  38. pos = elemDrop.getBoundingClientRect();
  39. var center2X = Math.floor((pos.left + pos.right) / 2);
  40. var center2Y = Math.floor((pos.top + pos.bottom) / 2);
  41.  
  42. // mouse over dragged element and mousedown
  43. fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
  44. fireMouseEvent("mouseenter", elemDrag, center1X, center1Y);
  45. fireMouseEvent("mouseover", elemDrag, center1X, center1Y);
  46. fireMouseEvent("mousedown", elemDrag, center1X, center1Y);
  47.  
  48. // start dragging process over to drop target
  49. fireMouseEvent("dragstart", elemDrag, center1X, center1Y);
  50. fireMouseEvent("drag", elemDrag, center1X, center1Y);
  51. fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
  52. fireMouseEvent("drag", elemDrag, center2X, center2Y);
  53. fireMouseEvent("mousemove", elemDrop, center2X, center2Y);
  54.  
  55. // trigger dragging process on top of drop target
  56. fireMouseEvent("mouseenter", elemDrop, center2X, center2Y);
  57. fireMouseEvent("dragenter", elemDrop, center2X, center2Y);
  58. fireMouseEvent("mouseover", elemDrop, center2X, center2Y);
  59. fireMouseEvent("dragover", elemDrop, center2X, center2Y);
  60.  
  61. // release dragged element on top of drop target
  62. fireMouseEvent("drop", elemDrop, center2X, center2Y);
  63. fireMouseEvent("dragend", elemDrag, center2X, center2Y);
  64. fireMouseEvent("mouseup", elemDrag, center2X, center2Y);
  65. }
  66.  
  67. // To explain what broke in the original code, here is a comment
  68. // The original code targeted the thumbnail for dragging when that is no longer viable
  69. // Additionally, the timestamp is now two elements instead of one, so I fixed that
  70. let sortVideosByLength = (allAnchors, allDragPoints) => {
  71. let videos = [];
  72. for (let j = 0; j < allAnchors.length; j++) {
  73. let thumb = allAnchors[j];
  74. let drag = allDragPoints[j];
  75. let href = thumb.href;
  76. if (href && href.includes("&list=WL&")) {
  77. let timeSpan = thumb.querySelector("#text");
  78. let timeDigits = timeSpan.innerText.trim().split(":").reverse();
  79. var time = parseInt(timeDigits[0]);
  80. if (timeDigits[1]) time += parseInt(timeDigits[1]) * 60;
  81. if (timeDigits[2]) time += parseInt(timeDigits[2]) * 3600;
  82. videos.push({ anchor: drag, time: time, originalIndex: j });
  83. }
  84. }
  85.  
  86. if (videos.length > 1) {
  87. for (let j = 0; j < videos.length - 1; j++) {
  88. var smallestLength = 864000;
  89. var smallestIndex = -1;
  90. for (var k = j + 1; k < videos.length; k++) {
  91. if (
  92. videos[k].time < videos[j].time &&
  93. videos[k].time < smallestLength
  94. ) {
  95. smallestLength = videos[k].time;
  96. smallestIndex = k;
  97. }
  98. }
  99. if (smallestIndex > -1) {
  100. console.log("Drag " + smallestIndex + " to " + j);
  101. var elemDrag = videos[smallestIndex].anchor;
  102. var elemDrop = videos[j].anchor;
  103. simulateDrag(elemDrag, elemDrop);
  104. return j;
  105. }
  106. }
  107. return videos.length;
  108. }
  109. return 0;
  110. }
  111.  
  112.  
  113.  
  114. let autoScroll = async () => {
  115. let element = document.scrollingElement;
  116. let currentScroll = element.scrollTop;
  117. do {
  118. currentScroll = element.scrollTop;
  119. element.scrollTop = element.scrollHeight;
  120. await new Promise((r) => setTimeout(r, loopTime));
  121. } while (currentScroll != element.scrollTop);
  122. }
  123.  
  124. // There is an inherent limit in how fast you can sort the videos, due to Youtube refreshing
  125. // This limit also applies if you do it manually
  126. // It is also much worse if you have a lot of videos, for every 100 videos, it's about an extra 2-4 seconds, maybe longer
  127. let zeLoop = async () => {
  128. await autoScroll();
  129. let count = document.querySelectorAll("ytd-playlist-video-renderer").length;
  130. let currentMinimum = 0;
  131. while (true) {
  132. let allAnchors = document.querySelectorAll("div#content a#thumbnail.inline-block.ytd-thumbnail");
  133. let allDragPoints = document.querySelectorAll("yt-icon#reorder");
  134. await autoScroll();
  135. try {
  136. currentMinimum = sortVideosByLength(allAnchors, allDragPoints);
  137. } catch (e) {
  138. if (e instanceof TypeError) {
  139. console.log("Problem with loading, waiting a bit more.")
  140. await new Promise((r) => setTimeout(r, loopTime));
  141. currentMinimum = sortVideosByLength(allAnchors, allDragPoints); // If it somehow still dies, waits another full cycle
  142. }
  143. }
  144. if (currentMinimum === count) { // If your document is already partially sorted, this will break the code early
  145. console.log("Sort complete, or you didn't load all the videos. Video sorted: " + currentMinimum);
  146. break;
  147. }
  148. await autoScroll();
  149. }
  150. }
  151.  
  152. // If the loading time is for some reason hugely inconsistent, you can use this instead to do it one by one
  153. let zeWithoutLoop = () => {
  154. let allAnchors = document.querySelectorAll("div#content a#thumbnail.inline-block.ytd-thumbnail");
  155. let allDragPoints = document.querySelectorAll("yt-icon#reorder");
  156. sortVideosByLength(allAnchors, allDragPoints);
  157. }
  158.  
  159.  
  160.  
  161. /**
  162. * Generate menu container element
  163. */
  164. let renderContainerElement = () => {
  165. const element = document.createElement('div')
  166. element.className = 'sort-playlist'
  167. element.style.paddingBottom = '16px'
  168.  
  169. document.querySelector('div.thumbnail-and-metadata-wrapper').append(element)
  170. }
  171.  
  172. /**
  173. * Generate button element
  174. * @param {function} click - OnClick handler
  175. * @param {String=} label - Button Label
  176. */
  177. let renderButtonElement = (click = () => {}, label = '') => {
  178. // Create button
  179. const element = document.createElement('button')
  180. element.className = 'style-scope sort-button-wl'
  181. element.innerText = label
  182. element.onclick = click
  183.  
  184. // Render button
  185. document.querySelector('div.sort-playlist').appendChild(element)
  186. }
  187.  
  188. let addCssStyle = () => {
  189. const element = document.createElement('style')
  190. element.innerHTML = `
  191. .sort-button-wl {
  192. background-color: #30d030;
  193. border: 1px #a0a0a0;
  194. border-radius: 2px;
  195. padding: 3px;
  196. margin: 3px;
  197. cursor: pointer;
  198. }
  199.  
  200. .sort-button-wl:active {
  201. background-color: #209020;
  202. }
  203.  
  204. `
  205. document.head.appendChild(element);
  206. }
  207.  
  208. // TODO: expose this in GUI
  209. // change this if it takes longer to load on your system
  210. let loopTime = 1500;
  211.  
  212. (function() {
  213. 'use strict';
  214. onElementReady('div.thumbnail-and-metadata-wrapper', false, () => {
  215. renderContainerElement();
  216. addCssStyle();
  217. renderButtonElement(zeLoop,'Sort All');
  218. renderButtonElement(zeWithoutLoop,'Sort One');
  219. })
  220. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址