Sort Youtube Watch Later by Duration

As the name implies, sorts youtube watch later by duration

目前为 2022-11-25 提交的版本。查看 最新版本

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

QingJ © 2025

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