Sort Youtube Watch Later by Duration

As the name implies, sorts youtube watch later by duration

Versión del día 25/11/2022. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// 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://greasyfork.org/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');
	})
})();