Greasy Fork is available in English.

Sort Youtube Watch Later by Duration

As the name implies, sorts youtube watch later by duration

Verze ze dne 20. 06. 2022. Zobrazit nejnovější verzi.

// Changelog 20/6:
// Added autoscroll cause apparently it doesn't do so on my instance of Firefox
// Added buttons this time
// Moved the code to be GreaseMonkey/TamperMonkey compatible

/* jshint esversion: 8 */
// ==UserScript==
// @name              Sort Youtube Watch Later by Duration
// @namespace         https://gist.github.com/KohGeek/65ad9e0118ee5f5ee484676731bcd092
// @version           v1.0
// @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.zukizuki.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, quantaToWait * 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');
    })
})();