Sort Youtube Watch Later by Duration

As the name implies, sorts youtube watch later by duration

目前为 2022-06-24 提交的版本。查看 最新版本

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

QingJ © 2025

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