Sort Youtube Watch Later by Duration

As the name implies, sorts youtube watch later by duration

目前為 2022-06-20 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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