Sort Youtube Watch Later by Duration

As the name implies, sorts youtube watch later by duration

Від 25.11.2022. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// 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');
	})
})();