Sort Youtube Watch Later by Duration

As the name implies, sorts youtube watch later by duration

当前为 2022-06-24 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// Changelog 24/6:
// Autoscroll delay now is not correlated with number of items in playlist
 
/* jshint esversion: 8 */
// ==UserScript==
// @name              Sort Youtube Watch Later by Duration
// @namespace         https://gist.github.com/KohGeek/65ad9e0118ee5f5ee484676731bcd092
// @version           1.0.3
// @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 = 86400;
            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;
}
 
// 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 () => {
    let count = document.querySelectorAll("ytd-playlist-video-renderer").length;
    let element = document.scrollingElement;
    let quantaToWait = Math.max(0, Math.ceil((count - 100)/100)); // about 2600 ms of load per 100 videos
    let currentMinimum = 0;
    while (true) {
        let allAnchors = document.querySelectorAll("div#content a#thumbnail.inline-block.ytd-thumbnail");
        let allDragPoints = document.querySelectorAll("yt-icon#reorder");
        let currentScroll = element.scrollTop;
        do {
            currentScroll = element.scrollTop;
            element.scrollTop = element.scrollHeight;
            await new Promise((r) => setTimeout(r, 1000));
        } while (currentScroll != element.scrollTop);
        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, quantaToWait * 1000));
                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 new Promise((r) => setTimeout(r, quantaToWait * 2500)); //Please set this time as needed, youtube refreshes everytime the WL gets changed
    }
}
 
// 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('ytd-playlist-sidebar-secondary-info-renderer.ytd-playlist-sidebar-renderer').prepend(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'
    element.style.backgroundColor = '#30d030'
    element.style.border = '1px #a0a0a0'
    element.style.borderRadius = '2px'
    element.style.padding = '3px'
    element.style.margin = '3px'
    element.style.cursor = 'pointer'
    element.innerText = label
    element.onclick = click
 
    // Render button
    document.querySelector('div.sort-playlist').appendChild(element)
}
 
(function() {
    'use strict';
    onElementReady('ytd-playlist-sidebar-secondary-info-renderer.ytd-playlist-sidebar-renderer', false, () => {
        renderContainerElement();
        renderButtonElement(zeLoop,'Sort All');
        renderButtonElement(zeWithoutLoop,'Sort One');
    })
})();