- // Changelog 25/11:
- // Youtube interface change broke the button, now fixed
-
- // Changelog 24/6:
- // Autoscroll delay now is not correlated with number of items in playlist
- // Autoscroll now triggers from the start
- // Added feedback to buttons
-
- /* jshint esversion: 8 */
- // ==UserScript==
- // @name Sort Youtube Watch Later by Duration
- // @namespace https://gist.github.com/KohGeek/65ad9e0118ee5f5ee484676731bcd092
- // @version 1.0.5
- // @description As the name implies, sorts youtube watch later by duration
- // @author KohGeek
- // @license GNU GPLv2
- // @match http://*.youtube.com/playlist*
- // @match https://*.youtube.com/playlist*
- // @require https://gf.qytechs.cn/scripts/374849-library-onelementready-es7/code/Library%20%7C%20onElementReady%20ES7.js
- // @grant none
- // @run-at document-start
- // ==/UserScript==
-
- // Heavily borrowed from many places
- // function for triggering mouse events
- let fireMouseEvent = (type, elem, centerX, centerY) => {
- var evt = document.createEvent("MouseEvents");
- evt.initMouseEvent(type, true, true, window, 1, 1, 1, centerX, centerY, false, false, false, false, 0, elem);
- elem.dispatchEvent(evt);
- };
-
- // https://ghostinspector.com/blog/simulate-drag-and-drop-javascript-casperjs/
- let simulateDrag = (elemDrag, elemDrop) => {
- // calculate positions
- var pos = elemDrag.getBoundingClientRect();
- var center1X = Math.floor((pos.left + pos.right) / 2);
- var center1Y = Math.floor((pos.top + pos.bottom) / 2);
- pos = elemDrop.getBoundingClientRect();
- var center2X = Math.floor((pos.left + pos.right) / 2);
- var center2Y = Math.floor((pos.top + pos.bottom) / 2);
-
- // mouse over dragged element and mousedown
- fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
- fireMouseEvent("mouseenter", elemDrag, center1X, center1Y);
- fireMouseEvent("mouseover", elemDrag, center1X, center1Y);
- fireMouseEvent("mousedown", elemDrag, center1X, center1Y);
-
- // start dragging process over to drop target
- fireMouseEvent("dragstart", elemDrag, center1X, center1Y);
- fireMouseEvent("drag", elemDrag, center1X, center1Y);
- fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
- fireMouseEvent("drag", elemDrag, center2X, center2Y);
- fireMouseEvent("mousemove", elemDrop, center2X, center2Y);
-
- // trigger dragging process on top of drop target
- fireMouseEvent("mouseenter", elemDrop, center2X, center2Y);
- fireMouseEvent("dragenter", elemDrop, center2X, center2Y);
- fireMouseEvent("mouseover", elemDrop, center2X, center2Y);
- fireMouseEvent("dragover", elemDrop, center2X, center2Y);
-
- // release dragged element on top of drop target
- fireMouseEvent("drop", elemDrop, center2X, center2Y);
- fireMouseEvent("dragend", elemDrag, center2X, center2Y);
- fireMouseEvent("mouseup", elemDrag, center2X, center2Y);
- }
-
- // To explain what broke in the original code, here is a comment
- // The original code targeted the thumbnail for dragging when that is no longer viable
- // Additionally, the timestamp is now two elements instead of one, so I fixed that
- let sortVideosByLength = (allAnchors, allDragPoints) => {
- let videos = [];
- for (let j = 0; j < allAnchors.length; j++) {
- let thumb = allAnchors[j];
- let drag = allDragPoints[j];
- let href = thumb.href;
- if (href && href.includes("&list=WL&")) {
- let timeSpan = thumb.querySelector("#text");
- let timeDigits = timeSpan.innerText.trim().split(":").reverse();
- var time = parseInt(timeDigits[0]);
- if (timeDigits[1]) time += parseInt(timeDigits[1]) * 60;
- if (timeDigits[2]) time += parseInt(timeDigits[2]) * 3600;
- videos.push({ anchor: drag, time: time, originalIndex: j });
- }
- }
-
- if (videos.length > 1) {
- for (let j = 0; j < videos.length - 1; j++) {
- var smallestLength = 864000;
- var smallestIndex = -1;
- for (var k = j + 1; k < videos.length; k++) {
- if (
- videos[k].time < videos[j].time &&
- videos[k].time < smallestLength
- ) {
- smallestLength = videos[k].time;
- smallestIndex = k;
- }
- }
- if (smallestIndex > -1) {
- console.log("Drag " + smallestIndex + " to " + j);
- var elemDrag = videos[smallestIndex].anchor;
- var elemDrop = videos[j].anchor;
- simulateDrag(elemDrag, elemDrop);
- return j;
- }
- }
- return videos.length;
- }
- return 0;
- }
-
-
-
- let autoScroll = async () => {
- let element = document.scrollingElement;
- let currentScroll = element.scrollTop;
- do {
- currentScroll = element.scrollTop;
- element.scrollTop = element.scrollHeight;
- await new Promise((r) => setTimeout(r, loopTime));
- } while (currentScroll != element.scrollTop);
- }
-
- // There is an inherent limit in how fast you can sort the videos, due to Youtube refreshing
- // This limit also applies if you do it manually
- // 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
- let zeLoop = async () => {
- await autoScroll();
- let count = document.querySelectorAll("ytd-playlist-video-renderer").length;
- let currentMinimum = 0;
- while (true) {
- let allAnchors = document.querySelectorAll("div#content a#thumbnail.inline-block.ytd-thumbnail");
- let allDragPoints = document.querySelectorAll("yt-icon#reorder");
- await autoScroll();
- try {
- currentMinimum = sortVideosByLength(allAnchors, allDragPoints);
- } catch (e) {
- if (e instanceof TypeError) {
- console.log("Problem with loading, waiting a bit more.")
- await new Promise((r) => setTimeout(r, loopTime));
- currentMinimum = sortVideosByLength(allAnchors, allDragPoints); // If it somehow still dies, waits another full cycle
- }
- }
- if (currentMinimum === count) { // If your document is already partially sorted, this will break the code early
- console.log("Sort complete, or you didn't load all the videos. Video sorted: " + currentMinimum);
- break;
- }
- await autoScroll();
- }
- }
-
- // If the loading time is for some reason hugely inconsistent, you can use this instead to do it one by one
- let zeWithoutLoop = () => {
- let allAnchors = document.querySelectorAll("div#content a#thumbnail.inline-block.ytd-thumbnail");
- let allDragPoints = document.querySelectorAll("yt-icon#reorder");
- sortVideosByLength(allAnchors, allDragPoints);
- }
-
-
-
- /**
- * Generate menu container element
- */
- let renderContainerElement = () => {
- const element = document.createElement('div')
- element.className = 'sort-playlist'
- element.style.paddingBottom = '16px'
-
- document.querySelector('div.thumbnail-and-metadata-wrapper').append(element)
- }
-
- /**
- * Generate button element
- * @param {function} click - OnClick handler
- * @param {String=} label - Button Label
- */
- let renderButtonElement = (click = () => {}, label = '') => {
- // Create button
- const element = document.createElement('button')
- element.className = 'style-scope sort-button-wl'
- element.innerText = label
- element.onclick = click
-
- // Render button
- document.querySelector('div.sort-playlist').appendChild(element)
- }
-
- let addCssStyle = () => {
- const element = document.createElement('style')
- element.innerHTML = `
- .sort-button-wl {
- background-color: #30d030;
- border: 1px #a0a0a0;
- border-radius: 2px;
- padding: 3px;
- margin: 3px;
- cursor: pointer;
- }
-
- .sort-button-wl:active {
- background-color: #209020;
- }
-
- `
- document.head.appendChild(element);
- }
-
- // TODO: expose this in GUI
- // change this if it takes longer to load on your system
- let loopTime = 1500;
-
- (function() {
- 'use strict';
- onElementReady('div.thumbnail-and-metadata-wrapper', false, () => {
- renderContainerElement();
- addCssStyle();
- renderButtonElement(zeLoop,'Sort All');
- renderButtonElement(zeWithoutLoop,'Sort One');
- })
- })();