您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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或关注我们的公众号极客氢云获取最新地址