YouTube Music: sort by play count (text as icon)

Truly sort songs from an artist's page by play count from highest to lowest (use OrangeMonkey if it does not work with TamperMonkey).

Ekde 2025/03/30. Vidu La ĝisdata versio.

// ==UserScript==
// @name         YouTube Music: sort by play count (text as icon)
// @match        https://music.youtube.com/* 
// @grant        none
// @version      1.0.12
// @license      MIT
// @description  Truly sort songs from an artist's page by play count from highest to lowest (use OrangeMonkey if it does not work with TamperMonkey).
// @namespace    https://github.com/KenKaneki73985
// @author       Ken Kaneki 
// ==/UserScript==
// user_script = "moz-extension://762e4395-b145-4620-8dd9-31bf09e052de/options.html#nav=196e172f-1c30-4404-877d-76dd37c37a9e" <--- this line is very important. Do not delete this at all cost.

(function() {
    'use strict';
    
    // ===== CONFIGURATION OPTIONS =====
    const NOTIFICATION_CONFIG = {
        IN_PROGRESS_Notification: {
            top: '80%',    // Vertical position (can use %, px, etc.)
            right: '38%',  // Horizontal position (can use %, px, etc.)
            fontSize: '17px'
        },

        SORTING_COMPLETE_Notification: {
            top: '80%',    // Different vertical position 
            right: '45%',  // Horizontal position
            fontSize: '17px'
        },

        ALREADY_SORTED_Notification: {
            top: '80%',    // Different vertical position 
            right: '41.5%',  // Horizontal position
            fontSize: '17px'
        }
    };
    
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        // Create a style for the notification
        const style = document.createElement('style');
        style.textContent = `
            #auto-dismiss-notification {
                position: fixed;
                color: white;
                padding: 15px;
                border-radius: 5px;
                z-index: 9999;
                transition: opacity 0.5s ease-out;
            }
            #auto-dismiss-notification.sorting-in-progress {
                background-color: rgba(0, 100, 0, 0.7); /* Green */
            }
            #auto-dismiss-notification.sorting-complete {
                background-color: rgba(82, 82, 255, 0.7); /* Blue */
            }
            #auto-dismiss-notification.already-sorted {
                // background-color: rgba(255, 165, 0, 0.7); /* Orange */
                background-color: rgba(82, 82, 255, 0.7); /* Blue */
            }`;
        document.head.appendChild(style);

        let SORT_SONGS_BTN = document.createElement('button')
        // SORT_SONGS_BTN.innerHTML ='<svg width="30px" height="30px" fill="#0080ff" viewBox="0 0 24 24" id="sort-ascending" data-name="Flat Line" xmlns="http://www.w3.org/2000/svg" class="icon flat-line" stroke="#0080ff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><polyline id="primary" points="10 15 6 19 2 15" style="fill: none; stroke: #0080ff; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></polyline><path id="primary-2" data-name="primary" d="M6,19V4M20,16H15m5-5H13m7-5H10" style="fill: none; stroke: #0080ff; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></path></g></svg>'
        SORT_SONGS_BTN.innerHTML ='sort'
        SORT_SONGS_BTN.style.color = "white"
        SORT_SONGS_BTN.style.fontSize = '12px';
        // SORT_SONGS_BTN.style.border = "none"
        SORT_SONGS_BTN.style.border = "solid 1px"
        SORT_SONGS_BTN.style.position = 'absolute'
        // SORT_SONGS_BTN.style.left = '89%' // works in 125/150%
        SORT_SONGS_BTN.style.left = '83%' // works in 125/150%?
        SORT_SONGS_BTN.style.top = '2.5%'
        SORT_SONGS_BTN.style.padding = '5px'
        SORT_SONGS_BTN.style.background = "none"
        SORT_SONGS_BTN.style.zIndex = '9999'
        SORT_SONGS_BTN.addEventListener('click', () => {
            // Check if playlist is already sorted
            if (IS_PLAYLIST_SORTED()) {
                MESSAGE_ALREADY_SORTED();
                return;
            }

            // Show message immediately
            MESSAGE_SORTING_IN_PROCESS();
            
            // Delay sorting to ensure message is visible
            setTimeout(() => {
                SORT_SONGS();
                
                // Show sorting complete message after sorting
                setTimeout(() => {
                    MESSAGE_SORTING_COMPLETE();
                }, 500);  // Small delay to ensure sorting is visually complete
            }, 50);  // Small delay to ensure message rendering
        });
        
        document.body.appendChild(SORT_SONGS_BTN)
    }
    
    // Function to convert play count string to number
    function parsePlayCount(playString) {
        playString = playString.replace(' plays', '').trim();
        
        const multipliers = {
            'B': 1000000000,
            'M': 1000000,
            'K': 1000
        };
        
        const match = playString.match(/^(\d+(?:\.\d+)?)\s*([BMK])?$/);
        
        if (!match) return 0;
        
        const number = parseFloat(match[1]);
        const multiplier = match[2] ? multipliers[match[2]] : 1;
        
        return number * multiplier;
    }
    
    // Check if playlist is already sorted by play count
    function IS_PLAYLIST_SORTED() {
        const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
        
        if (PLAYLIST_SHELF_DIV) {
            const children = Array.from(PLAYLIST_SHELF_DIV.children);
            
            const playCounts = children.map(child => {
                const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
                return playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0;
            });
            
            // Check if play counts are in descending order
            for (let i = 1; i < playCounts.length; i++) {
                if (playCounts[i] > playCounts[i - 1]) {
                    return false; // Not sorted
                }
            }
            
            return true; // Already sorted
        }
        
        return false;
    }
    
    function SORT_SONGS(){
        const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
        
        if (PLAYLIST_SHELF_DIV) {
            // Clone the original children to preserve event listeners
            const topLevelChildren = Array.from(PLAYLIST_SHELF_DIV.children);
            
            const songInfo = [];
            
            topLevelChildren.forEach((child, index) => {
                const titleElement = child.querySelector('div:nth-child(5) > div:nth-child(1) > yt-formatted-string:nth-child(1) > a:nth-child(1)');
                const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
                
                const songDetails = {
                    element: child,
                    id: `${index + 1}`,
                    title: titleElement ? titleElement.textContent.trim() : 'Title not found',
                    plays: playsElement ? playsElement.textContent.trim() : 'Plays not found',
                    playCount: playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0
                };
                
                songInfo.push(songDetails);
            });
            
            // Sort songs by play count (highest to lowest)
            songInfo.sort((a, b) => b.playCount - a.playCount);
            
            // Use replaceChildren to preserve original event listeners
            PLAYLIST_SHELF_DIV.replaceChildren(...songInfo.map(song => song.element));
            
            // Modify song ranks without recreating elements
            songInfo.forEach((song, index) => {
                song.element.id = `${index + 1}`;
            });
            
            console.log("Success: Sorted By Play Count");
        } else {
            alert('error: Playlist shelf div not found');
        }
    }

    function MESSAGE_SORTING_IN_PROCESS(){
        // Remove any existing notification
        const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
        if (EXISTING_NOTIFICATION) {
            EXISTING_NOTIFICATION.remove();
        }

        // Create new notification element
        const notification = document.createElement('div');
        notification.id = 'auto-dismiss-notification';
        notification.classList.add('sorting-in-progress');
        notification.textContent = "Sorting in Progress... Wait a few seconds"

        // Apply configuration
        notification.style.top = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.top;
        notification.style.right = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.right;
        notification.style.fontSize = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.fontSize;

        // Append to body
        document.body.appendChild(notification);

        // Auto-dismiss after 3 seconds
        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                notification.remove();
            }, 500); // matches transition time
        }, 3000);
    }

    function MESSAGE_SORTING_COMPLETE(){
        // Remove any existing notification
        const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
        if (EXISTING_NOTIFICATION) {
            EXISTING_NOTIFICATION.remove();
        }

        // Create new notification element
        const notification = document.createElement('div');
        notification.id = 'auto-dismiss-notification';
        notification.classList.add('sorting-complete');
        notification.textContent = "Sorting Complete"

        // Apply configuration
        notification.style.top = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.top;
        notification.style.right = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.right;
        notification.style.fontSize = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.fontSize;

        // Append to body
        document.body.appendChild(notification);

        // Auto-dismiss after 3 seconds
        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                notification.remove();
            }, 500); // matches transition time
        }, 3000);
    }

    function MESSAGE_ALREADY_SORTED(){
        // Remove any existing notification
        const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
        if (EXISTING_NOTIFICATION) {
            EXISTING_NOTIFICATION.remove();
        }

        // Create new notification element
        const notification = document.createElement('div');
        notification.id = 'auto-dismiss-notification';
        notification.classList.add('already-sorted');
        // notification.textContent = "Playlist Already Sorted by Play Count"
        notification.textContent = "Already Sorted by Play Count"

        // Apply configuration
        notification.style.top = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.top;
        notification.style.right = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.right;
        notification.style.fontSize = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.fontSize;

        // Append to body
        document.body.appendChild(notification);

        // Auto-dismiss after 3 seconds
        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                notification.remove();
            }, 500); // matches transition time
        }, 3000);
    }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。