YT Playlist Sorter

Sorts youtube playlist by duration

// ==UserScript==

/* jshint esversion: 8 */
// ==UserScript==
// @name              YT Playlist Sorter
// @namespace         https://github.com/cloph-dsp/SortYoutubePlaylistByDuration
// @version           4.0
// @description       Sorts youtube playlist by duration
// @author            cloph-dsp
// @originalAuthor    KohGeek
// @license           GPL-2.0-only
// @match             http://*.youtube.com/*
// @match             https://*.youtube.com/*
// @require           https://gf.qytechs.cn/scripts/374849-library-onelementready-es7/code/Library%20%7C%20onElementReady%20ES7.js
// @supportURL        https://github.com/cloph-dsp/SortYoutubePlaylistByDuration
// @grant             none
// @run-at            document-start
// ==/UserScript==

// CSS styles
const css = `
        .sort-playlist-div {
            font-size: 12px;
            padding: 3px 1px;
            display: flex;
            align-items: center;
        }
        .sort-playlist {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        .sort-playlist-controls {
            display: flex;
            align-items: center;
            gap: 12px;
        }
        .sort-button-toggle {
            border: 1px solid #a0a0a0;
            border-radius: 4px;
            padding: 8px 16px;
            cursor: pointer;
            font-weight: bold;
            font-size: 14px;
            transition: all 0.2s ease;
            box-shadow: 0 1px 2px rgba(0,0,0,0.1);
        }
        .sort-button-start {
            background-color: #30d030;
            color: white;
        }
        .sort-button-stop {
            background-color: #d03030;
            color: white;
        }
        .sort-button-start:hover {
            background-color: #28b828;
            transform: translateY(-1px);
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        }
        .sort-button-stop:hover {
            background-color: #b82828;
            transform: translateY(-1px);
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        }
        .sort-select {
            border: 1px solid #a0a0a0;
            border-radius: 4px;
            padding: 8px 12px;
            font-size: 14px;
            background-color: #f8f8f8;
            cursor: pointer;
            transition: all 0.2s ease;
        }
        .sort-select:hover {
            border-color: #666;
            background-color: #f0f0f0;
        }
        .sort-select:focus {
            outline: none;
            border-color: #30d030;
            box-shadow: 0 0 0 2px rgba(48, 208, 48, 0.2);
        }
        .sort-log {
            padding: 8px;
            margin-top: 8px;
            border-radius: 5px;
            background-color: #202020;
            color: #e0e0e0;
            font-size: 13px;
        }
    `

const modeAvailable = [
    { value: 'asc', label: 'by Shortest' },
    { value: 'desc', label: 'by Longest' }
];

const debug = false;

// Config
let isSorting = false;
let scrollLoopTime = 500;
let useAdaptiveDelay = true;
let baseDelayPerVideo = 5;
let minDelay = 10;
let maxDelay = 1000;
let fastModeThreshold = 200;

let sortMode = 'asc';
let autoScrollInitialVideoList = true;
let log = document.createElement('div');
let stopSort = false;

// Fire mouse event at coordinates
let fireMouseEvent = (type, elem, centerX, centerY) => {
    const event = new MouseEvent(type, {
        view: window,
        bubbles: true,
        cancelable: true,
        clientX: centerX,
        clientY: centerY
    });

    elem.dispatchEvent(event);
};

// Simulate drag between elements
let simulateDrag = (elemDrag, elemDrop) => {
    // Get positions
    let pos = elemDrag.getBoundingClientRect();
    let center1X = Math.floor((pos.left + pos.right) / 2);
    let center1Y = Math.floor((pos.top + pos.bottom) / 2);
    pos = elemDrop.getBoundingClientRect();
    let center2X = Math.floor((pos.left + pos.right) / 2);
    let center2Y = Math.floor((pos.top + pos.bottom) / 2);

    // Mouse events for dragged element
    fireMouseEvent("mousemove", elemDrag, center1X, center1Y);
    fireMouseEvent("mouseenter", elemDrag, center1X, center1Y);
    fireMouseEvent("mouseover", elemDrag, center1X, center1Y);
    fireMouseEvent("mousedown", elemDrag, center1X, center1Y);

    // Start drag
    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);

    // Events over drop target
    fireMouseEvent("mouseenter", elemDrop, center2X, center2Y);
    fireMouseEvent("dragenter", elemDrop, center2X, center2Y);
    fireMouseEvent("mouseover", elemDrop, center2X, center2Y);
    fireMouseEvent("dragover", elemDrop, center2X, center2Y);

    // Complete drop
    fireMouseEvent("drop", elemDrop, center2X, center2Y);
    fireMouseEvent("dragend", elemDrag, center2X, center2Y);
    fireMouseEvent("mouseup", elemDrag, center2X, center2Y);
};

// Scroll to position or page bottom
let autoScroll = async (scrollTop = null) => {
    let element = document.scrollingElement;
    let scrollDestination = scrollTop !== null ? scrollTop : element.scrollHeight;
    let startPosition = element.scrollTop;

    logActivity(`Scrolling page... 📜`);
    
    // Use stepped approach for longer scrolls
    if (Math.abs(startPosition - scrollDestination) > 3000) {
        const totalSteps = 4;
        const stepSize = (scrollDestination - startPosition) / totalSteps;
        
        for (let i = 1; i <= totalSteps && !stopSort; i++) {
            const intermediatePosition = startPosition + (stepSize * i);
            
            element.scrollTo({
                top: intermediatePosition,
                behavior: 'smooth'
            });
            
            await new Promise(r => setTimeout(r, Math.max(350, scrollLoopTime * 0.5)));
        }
        
        // Final adjustment
        if (Math.abs(element.scrollTop - scrollDestination) > 50 && !stopSort) {
            element.scrollTop = scrollDestination;
            await new Promise(r => setTimeout(r, Math.max(500, scrollLoopTime * 0.6)));
        }
    } else {
        // For shorter distances
        try {
            element.scrollTo({
                top: scrollDestination,
                behavior: 'smooth'
            });
            
            await new Promise(r => setTimeout(r, Math.max(350, scrollLoopTime * 0.6)));
            
            if (Math.abs(element.scrollTop - scrollDestination) > 50 && !stopSort) {
                element.scrollTop = scrollDestination;
                await new Promise(r => setTimeout(r, Math.max(450, scrollLoopTime * 0.7)));
            }
        } catch (e) {
            element.scrollTop = scrollDestination;
            await new Promise(r => setTimeout(r, Math.max(550, scrollLoopTime * 0.8)));
        }
    }
};

// Log message to UI
let logActivity = (message) => {
    log.innerText = message;
    if (debug) {
        console.log(message);
    }
};

// Create UI container
let renderContainerElement = () => {
    const element = document.createElement('div')
    element.className = 'sort-playlist sort-playlist-div'
    element.style.paddingBottom = '16px'

    const controlsContainer = document.createElement('div')
    controlsContainer.className = 'sort-playlist-controls'
    element.appendChild(controlsContainer)

    document.querySelector('div.thumbnail-and-metadata-wrapper').append(element)
}

// Create sort toggle button
let renderToggleButton = () => {
    const element = document.createElement('button');
    element.id = 'sort-toggle-button';
    element.className = 'style-scope sort-button-toggle sort-button-start';
    element.innerText = 'Sort Videos';
    
    element.onclick = async () => {
        if (!isSorting) {
            // Start
            isSorting = true;
            element.className = 'style-scope sort-button-toggle sort-button-stop';
            element.innerText = 'Stop Sorting';
            await activateSort();
            // Reset
            isSorting = false;
            element.className = 'style-scope sort-button-toggle sort-button-start';
            element.innerText = 'Sort Videos';
        } else {
            // Stop
            stopSort = true;
            isSorting = false;
            element.className = 'style-scope sort-button-toggle sort-button-start';
            element.innerText = 'Sort Videos';
        }
    };

    document.querySelector('.sort-playlist-controls').appendChild(element);
};

// Create dropdown selector
let renderSelectElement = (variable = 0, options = [], label = '') => {
    const element = document.createElement('select');
    element.className = 'style-scope sort-select';
    element.onchange = (e) => {
        if (variable === 0) {
            sortMode = e.target.value;
        } else if (variable === 1) {
            autoScrollInitialVideoList = e.target.value;
        }
    };

    options.forEach((option) => {
        const optionElement = document.createElement('option')
        optionElement.value = option.value
        optionElement.innerText = option.label
        element.appendChild(optionElement)
    });

    document.querySelector('.sort-playlist-controls').appendChild(element);
};

// Create number input
let renderNumberElement = (defaultValue = 0, label = '') => {
    const elementDiv = document.createElement('div');
    elementDiv.className = 'sort-playlist-div sort-margin-right-3px';
    elementDiv.innerText = label;

    const element = document.createElement('input');
    element.type = 'number';
    element.value = defaultValue;
    element.className = 'style-scope';
    element.oninput = (e) => { scrollLoopTime = +(e.target.value) };

    elementDiv.appendChild(element);
    document.querySelector('div.sort-playlist').appendChild(elementDiv);
};

// Create status log display
let renderLogElement = () => {
    log.className = 'style-scope sort-log';
    log.innerText = 'Please wait for the playlist to fully load before sorting...';
    document.querySelector('div.sort-playlist').appendChild(log);
};

// Add CSS to page
let addCssStyle = () => {
    const element = document.createElement('style');
    element.textContent = css;
    document.head.appendChild(element);
};

// Wait for YouTube to process drag
let waitForYoutubeProcessing = async () => {
    const maxWaitTime = Math.max(scrollLoopTime * 3, 1000);
    const startTime = Date.now();
    let isProcessed = false;
    
    logActivity(`Rearranging playlist... ⏳`);
    
    // Fast polling
    const pollInterval = Math.min(50, Math.max(25, scrollLoopTime * 0.1));
    
    while (!isProcessed && Date.now() - startTime < maxWaitTime && !stopSort) {
        await new Promise(r => setTimeout(r, pollInterval));
        
        const allDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
        const currentItems = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
        
        if (allDragPoints.length > 0 && currentItems.length > 0 && 
            document.querySelectorAll('.ytd-continuation-item-renderer').length === 0) {
            isProcessed = true;
            const timeTaken = Date.now() - startTime;
            if (timeTaken > 1000) {
                logActivity(`Video moved successfully ✓`);
            }
            
            // Stabilization wait
            await new Promise(r => setTimeout(r, Math.min(180, scrollLoopTime * 0.25)));
            return true;
        }
    }
    
    // Recovery methods

    // Fast recovery approach
    if (!isProcessed) {
        logActivity(`Ensuring YouTube updates... ⏱️`);
        
        // Click body to refocus
        document.body.click();
        await new Promise(r => setTimeout(r, 275));
        
        // Check results
        let updatedDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
        let allThumbnails = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
        
        if (updatedDragPoints.length > 0 && allThumbnails.length > 0) {
            logActivity(`Recovered playlist view ✓`);
            await new Promise(r => setTimeout(r, 540));
            
            // Force refresh
            window.dispatchEvent(new Event('resize'));
            await new Promise(r => setTimeout(r, 180));
            
            return true;
        }
        
        // Try faster recovery (2 attempts)
        for (let i = 0; i < 2 && !stopSort; i++) {
            const areas = [
                document.querySelector('.playlist-items'), 
                document.body
            ];
            
            // Click areas
            for (const area of areas) {
                if (area && !stopSort) {
                    area.click();
                    await new Promise(r => setTimeout(r, 125));
                }
            }
            
            // Slight scroll
            if (!stopSort) {
                const scrollElem = document.scrollingElement;
                const currentPos = scrollElem.scrollTop;
                scrollElem.scrollTop = currentPos + 10; 
                await new Promise(r => setTimeout(r, 125));
                scrollElem.scrollTop = currentPos;
            }
            
            // Check if successful
            updatedDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
            allThumbnails = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
            
            if (updatedDragPoints.length > 0 && allThumbnails.length > 0) {
                logActivity(`Recovered after attempt ${i+1} ✓`);
                await new Promise(r => setTimeout(r, (540 + i * 125)));
                
                // Force refresh
                window.dispatchEvent(new Event('resize'));
                await new Promise(r => setTimeout(r, 175));
                
                return true;
            }
            
            await new Promise(r => setTimeout(r, 315 + i * 175));
        }
        
        // Final fallback
        if (!stopSort) {
            logActivity(`Attempting final recovery method...`);
            
            document.body.click();
            await new Promise(r => setTimeout(r, 225));
            
            const playlistHeader = document.querySelector('ytd-playlist-header-renderer');
            if (playlistHeader) playlistHeader.click();
            
            await new Promise(r => setTimeout(r, 725));
            // Final check
            updatedDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
            allThumbnails = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
            if (updatedDragPoints.length > 0 && allThumbnails.length > 0) {
                logActivity(`Final recovery successful ✓`);
                await new Promise(r => setTimeout(r, 700)); 
                // Force refresh
                window.dispatchEvent(new Event('resize'));
                await new Promise(r => setTimeout(r, 175));
                return true;
            }
            logActivity(`Recovery failed - skipping operation`);
            return false;
        }
    }
    return isProcessed;
};

// Check if playlist is fully loaded
let isPlaylistFullyLoaded = (reportedCount, loadedCount) => {
    const hasSpinner = document.querySelector('.ytd-continuation-item-renderer') !== null;
    return reportedCount === loadedCount && !hasSpinner;
};

// Check if YouTube page is ready
let isYouTubePageReady = () => {
    const playlistContainer = document.querySelector("ytd-item-section-renderer:first-of-type");
    const videoCount = document.querySelector(".metadata-stats span.yt-formatted-string:first-of-type");
    const dragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
    
    // Spinner detection
    const spinnerElements = document.querySelectorAll('.ytd-continuation-item-renderer, yt-icon-button.ytd-continuation-item-renderer, .circle.ytd-spinner');
    const hasVisibleSpinner = Array.from(spinnerElements).some(el => {
        const rect = el.getBoundingClientRect();
        return (rect.width > 0 && rect.height > 0 && 
                rect.top >= 0 && rect.top <= window.innerHeight);
    });
    
    const reportedCount = videoCount ? Number(videoCount.innerText) : 0;
    const loadedCount = dragPoints.length;
    const youtubeLoadLimit = 100;
    
    // Check readiness
    const basicElementsReady = playlistContainer && videoCount && loadedCount > 0;
    const hasEnoughVideos = loadedCount >= 95 || loadedCount === reportedCount;
    const fullyLoaded = (!hasVisibleSpinner && hasEnoughVideos) || 
                         (loadedCount >= 0.97 * Math.min(reportedCount, youtubeLoadLimit));
    
    const isReady = basicElementsReady && fullyLoaded;
    
    // Show status
    if (isReady) {
        if (loadedCount < reportedCount) {
            logActivity(`✅ Ready: ${loadedCount}/${reportedCount} videos (YouTube limit reached)`);
        } else {
            logActivity(`✅ Ready: ${loadedCount}/${reportedCount} videos`);
        }
    } else if (basicElementsReady) {
        if (hasVisibleSpinner) {
            logActivity(`⏳ Loading: ${loadedCount}/${reportedCount} videos`);
        } else if (loadedCount < reportedCount) {
            logActivity(`⚠️ Some videos unavailable: ${loadedCount}/${reportedCount}`);
        } else {
            logActivity(`⌛ Waiting: ${loadedCount} videos`);
        }
    } else {
        logActivity(`🔄 Waiting: ${loadedCount}/${reportedCount || '?'} videos`);
    }
    return isReady;
};

// Sort videos by duration
let sortVideos = (allAnchors, allDragPoints, expectedCount) => {
    let videos = [];
    let sorted = 0;
    let dragged = false;
    
    if (allDragPoints.length !== expectedCount || allAnchors.length !== expectedCount) {
        logActivity("Playlist not fully loaded, waiting...");
        return 0;
    }
    
    // Get duration for each video
    for (let j = 0; j < allDragPoints.length; j++) {
        let thumb = allAnchors[j];
        let drag = allDragPoints[j];
        let timeSpan = thumb.querySelector("#text");
        let timeDigits = timeSpan.innerText.trim().split(":").reverse();
        let time;
        if (timeDigits.length == 1) {
            sortMode == "asc" ? time = 999999999999999999 : time = -1;
        } else {
            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 });
    }
    
    // Sort by duration
    if (sortMode == "asc") {
        videos.sort((a, b) => a.time - b.time);
    } else {
        videos.sort((a, b) => b.time - a.time);
    }
    
    // Drag operations
    for (let j = 0; j < videos.length; j++) {
        let originalIndex = videos[j].originalIndex;

        if (debug) {
            console.log("Loaded: " + videos.length + ". Current: " + j + ". Original: " + originalIndex + ".");
        }

        if (originalIndex !== j) {
            let elemDrag = videos[j].anchor;
            let elemDrop = videos.find((v) => v.originalIndex === j).anchor;

            logActivity("Drag " + originalIndex + " to " + j);
            simulateDrag(elemDrag, elemDrop);
            dragged = true;
        }

        sorted = j;

        if (stopSort || dragged) {
            break;
        }
    }

    return sorted;
}

// Main sorting function
let activateSort = async () => {
    // Set manual sorting mode
    const ensureManualSort = async () => {
        const sortButton = document.querySelector('yt-dropdown-menu[icon-label="Ordenar"] tp-yt-paper-button, yt-dropdown-menu[icon-label="Sort"] tp-yt-paper-button');
        if (!sortButton) {
            logActivity("Sort dropdown not found. Using current mode.");
            return;
        }
        
        sortButton.click();
        await new Promise(r => setTimeout(r, 300));
        
        const manualOption = document.querySelector('tp-yt-paper-listbox a:first-child tp-yt-paper-item');
        if (manualOption) {
            manualOption.click();
            logActivity("Switched to Manual sort mode");
            await new Promise(r => setTimeout(r, 500));
        } else {
            logActivity("Manual sort option not found. Using current mode.");
        }
    };

    await ensureManualSort();
    let reportedVideoCount = Number(document.querySelector(".metadata-stats span.yt-formatted-string:first-of-type").innerText);
    let allDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
    let allAnchors;
    
    // Set optimal delay based on playlist size
    if (useAdaptiveDelay) {
        const needsScrolling = reportedVideoCount > 95 && allDragPoints.length < reportedVideoCount && autoScrollInitialVideoList;
        
        if (needsScrolling) {
            scrollLoopTime = Math.max(500, minDelay * 8);
            logActivity(`Using scroll-safe speed (${scrollLoopTime}ms)`);
        } else {
            let videoCount = reportedVideoCount || allDragPoints.length;
            
            if (videoCount <= fastModeThreshold) {
                let fastDelay = Math.max(75, baseDelayPerVideo * Math.sqrt(videoCount * 0.75));
                scrollLoopTime = Math.min(fastDelay, 350);
                logActivity(`Using fast mode: ${scrollLoopTime}ms}`);
            } else {
                let calculatedDelay = Math.max(100, baseDelayPerVideo * Math.log(videoCount) * 2.5);
                scrollLoopTime = Math.min(calculatedDelay, 800);
                logActivity(`Using adaptive delay: ${scrollLoopTime}ms}`);
            }
        }
    }
    
    let sortedCount = 0;
    let initialVideoCount = allDragPoints.length;
    let scrollRetryCount = 0;
    stopSort = false;
    let consecutiveRecoveryFailures = 0;

    // Load all videos
    while (reportedVideoCount !== initialVideoCount
        && document.URL.includes("playlist?list=")
        && stopSort === false
        && autoScrollInitialVideoList === true) {
        logActivity(`Loading videos (${allDragPoints.length}/${reportedVideoCount})`);
        if (scrollRetryCount > 5) {
            break;
        } else if (scrollRetryCount > 0) {
            logActivity(`Some videos may be unavailable. Attempt: ${scrollRetryCount}/5`);
        }
        
        // Show appropriate warning based on playlist size
        if (allDragPoints.length > 600) {
            logActivity(`${allDragPoints.length} videos - large playlist warning (may take several minutes)`);
        } else if (allDragPoints.length > 300) {
            logActivity(`${allDragPoints.length} videos - this may take a while`);
        }
        
        await autoScroll();
        allDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
        initialVideoCount = allDragPoints.length;

        if (((reportedVideoCount - initialVideoCount) / 10) < 1) {
            scrollRetryCount++;
        }
    }
    logActivity(initialVideoCount + " videos loaded.");
    if (scrollRetryCount > 5) logActivity(log.innerText + "\nMax scroll attempts reached. Proceeding with available videos.");
    let loadedLocation = document.scrollingElement.scrollTop;
    scrollRetryCount = 0;

    // Sort videos
    const sortStartTime = Date.now();
    const maxSortTime = 900000; // 15 minutes max

    // Check timeout
    if (Date.now() - sortStartTime > maxSortTime) {
        logActivity("Sorting timed out after 15 minutes.");
        return;
    }
    
    while (sortedCount < initialVideoCount && stopSort === false) {
        // Check timeout
        if (Date.now() - sortStartTime > maxSortTime) {
            logActivity("Sorting timed out after 15 minutes.");
            return;
        }
        
        // Reset after recovery failures
        if (consecutiveRecoveryFailures >= 3) {
            logActivity("Too many failures. Reattempting...");
            await new Promise(r => setTimeout(r, 1500));
            consecutiveRecoveryFailures = 0;
        }
        allDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
        allAnchors = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
        scrollRetryCount = 0;

        // Ensure durations loaded
        while (!allAnchors[initialVideoCount - 1]?.querySelector("#text") && stopSort === false) {
            if (document.scrollingElement.scrollTop < loadedLocation && scrollRetryCount < 3) {
                logActivity(`Loading details... (${sortedCount + 1}/${initialVideoCount})`);
                await autoScroll(loadedLocation);
                scrollRetryCount++;
            } else {
                logActivity(`Scrolling for content...`);
                await autoScroll();
            }
            // Refresh references
            allDragPoints = document.querySelectorAll("ytd-item-section-renderer:first-of-type yt-icon#reorder");
            allAnchors = document.querySelectorAll("ytd-item-section-renderer:first-of-type div#content a#thumbnail.inline-block.ytd-thumbnail");
            if (allAnchors.length === 0 || allDragPoints.length === 0) {
                logActivity("Lost video references. Waiting...");
                await new Promise(r => setTimeout(r, 1000));
            }
        }

        // Sort if elements available
        if (allAnchors.length > 0 && allDragPoints.length > 0) {
            sortedCount = Number(sortVideos(allAnchors, allDragPoints, initialVideoCount) + 1);
            const processingSuccessful = await waitForYoutubeProcessing();
            if (!processingSuccessful) {
                logActivity("Recovery failed - waiting...");
                consecutiveRecoveryFailures++;
                await new Promise(r => setTimeout(r, 1000 + (consecutiveRecoveryFailures * 300)));
                continue;
            } else {
                consecutiveRecoveryFailures = 0;
            }
        } else {
            logActivity("No video elements. Waiting...");
            await new Promise(r => setTimeout(r, 1500));
            consecutiveRecoveryFailures++;
        }
    }
    
    // Final status
    if (stopSort === true) {
        logActivity("Sorting canceled ⛔");
        stopSort = false;
    } else {
        logActivity(`Sorting complete ✓ (${sortedCount} videos)`);
    }
};

// Initialize UI
let init = () => {
    onElementReady('div.thumbnail-and-metadata-wrapper', false, () => {
        autoScrollInitialVideoList = true;
        useAdaptiveDelay = true;
        
        renderContainerElement();
        addCssStyle();
        renderToggleButton();
        renderSelectElement(0, modeAvailable, 'Sort Order');
        renderLogElement();
        
        // Check page loading
        const checkPageInterval = setInterval(() => {
            if (isYouTubePageReady()) {
                logActivity("✓ Ready to sort");
                clearInterval(checkPageInterval);
            }
        }, 1000);
    });
};

// Initialize script
(() => {
    init();
    navigation.addEventListener('navigate', navigateEvent => {
        const url = new URL(navigateEvent.destination.url);
        if (url.pathname.includes('playlist?')) init();
    });
})();

QingJ © 2025

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