YouTube Music: sort by play count

Truly sort songs from an artist's page by play count from highest to lowest.

// ==UserScript==
// @name         YouTube Music: sort by play count
// @match        https://music.youtube.com/* 
// @grant        none
// @version      1.0.20
// @license      MIT
// @description  Truly sort songs from an artist's page by play count from highest to lowest.
// @namespace    https://github.com/KenKaneki73985
// @author       Ken Kaneki 
// ==/UserScript==
// user_script = "moz-extension://762e4395-b145-4620-8dd9-31bf09e052de/options.html#nav=d6bde39c-7fa5-41c7-bf85-3301be56dd30+editor" <--- this line is very important. Do not delete this at all cost.

(function() {
    'use strict';
    
    document.addEventListener('click', () => {

        if (SORT_TOGGLE){
            CLICK_SHOW_ALL_THEN_SORT()
        }

        async function CLICK_SHOW_ALL_THEN_SORT() {
            await new Promise(resolve => setTimeout(resolve, 1000))
            // beware of "show all button" for possible location change
            let SHOW_ALL_SONGS_BTN = document.querySelector("yt-button-shape.ytmusic-shelf-renderer > button:nth-child(1) > div:nth-child(1) > span:nth-child(1)") 
            if (SHOW_ALL_SONGS_BTN){
                SHOW_ALL_SONGS_BTN.click()
                MESSAGE_SORTING_IN_PROCESS()

                await new Promise(resolve => setTimeout(resolve, 1000))
                SORT_SONGS()
            } else {
                // console.log("not found > 'show all songs' button. No sorting.");
            }
        }
    })

    // ---------- CONFIGURATION OPTIONS ----------
    const NOTIFICATION_CONFIG = {
        IN_PROGRESS_Notification: {
            top: '82%',    // Vertical position (can use %, px, etc.)
            // right: '38%',  // "sorting in progress.. wait a few seconds"
            right: '44.5%',  // "sorting in progress"
            fontSize: '16px',
            padding: '0px'
        },

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

        ALREADY_SORTED_Notification: {
            // top: '85%',  
            // right: '42.5%',  // "already sorted by play count"
            // right: '46%',  // "already sorted"

            // ---------- TOP RIGHT ----------
            // top: '1.2%',    
            // right: '17%', 

            // ---------- BOTTOM OF SORT ICON ----------
            top: '8%',    
            right: '11%', 
            fontSize: '13px'
        }
    };

    // ---------------------- SORT BUTTON ----------------------
    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(82, 82, 255, 0.7); /* Blue */
                // background-color: rgba(0, 0, 0, 0.7); /* Black */
            }`;
        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.style.border = "none"
        // SORT_SONGS_BTN.style.position = 'absolute'
        SORT_SONGS_BTN.style.position = 'fixed'
        // SORT_SONGS_BTN.style.left = '89%' // works in 125/150%
        SORT_SONGS_BTN.style.left = '85.5%' // works in 125/150%?
        SORT_SONGS_BTN.style.top = '2.5%'
        SORT_SONGS_BTN.style.padding = '0px'
        SORT_SONGS_BTN.style.background = "none"
        SORT_SONGS_BTN.style.zIndex = '9999'

        // ---------------------- SORT BUTTON CLICK EVENT ----------------------
        SORT_SONGS_BTN.addEventListener('click', () => {
            // Check if playlist is already sorted
            if (IS_PLAYLIST_SORTED()) {
                MESSAGE_ALREADY_SORTED();
                return;
            }

            // IF NOT ALREADY SORTED
            MESSAGE_SORTING_IN_PROCESS();

            CLICK_SHOW_ALL_THEN_SORT()

            async function CLICK_SHOW_ALL_THEN_SORT() {
                await new Promise(resolve => setTimeout(resolve, 1000))
                let SHOW_ALL_SONGS_BTN = document.querySelector("yt-button-shape.ytmusic-shelf-renderer > button:nth-child(1) > div:nth-child(1) > span:nth-child(1)") 
                
                if (SHOW_ALL_SONGS_BTN){
                    SHOW_ALL_SONGS_BTN.click()
                    MESSAGE_SORTING_IN_PROCESS()

                    await new Promise(resolve => setTimeout(resolve, 1000)) // <--- will "await new Promise" work fine here?
                    SORT_SONGS()
                } 
                
                else {
                    SORT_SONGS()
                }
            }
        })
        
        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");
            MESSAGE_SORTING_COMPLETE()
        } else {
            console.log("error: Playlist shelf div not found");
            // 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"
        notification.textContent = "Sorting in Progress"

        // 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"

        // 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 2 seconds
        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                notification.remove();
            }, 500); // matches transition time
        }, 1000);
    }

    // ---------------------- TOGGLE BUTTON ----------------------
    // ---------- TOGGLE STATE ----------
    let SORT_TOGGLE = true;
    // ---------- CREATE TOGGLE BUTTON ----------
    function createToggleButton() {
        // Create button container
        const buttonContainer = document.createElement('div');
        buttonContainer.style.position = 'fixed';
        buttonContainer.style.top = '3.2%';
        buttonContainer.style.left = '76%';
        buttonContainer.style.zIndex = '9999';
        buttonContainer.style.backgroundColor = 'black';
        // buttonContainer.style.padding = '10px 15px';
        // buttonContainer.style.padding = '5px 10px';
        buttonContainer.style.padding = '3px 8px';
        buttonContainer.style.borderRadius = '8px';
        buttonContainer.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
        buttonContainer.style.display = 'flex';
        buttonContainer.style.alignItems = 'center';
        buttonContainer.style.gap = '0px';
        
        // Create label
        const label = document.createElement('span');
        label.textContent = 'AUTO SORT';

        // ---------- FONT STYLE ----------
        // label.style.fontFamily = 'Arial, sans-serif';
        label.style.fontFamily = 'SEGOE UI'; // changed from 'Arial, sans-serif'
        // label.style.fontFamily = 'TAHOMA'; // changed from 'Arial, sans-serif'
        // label.style.fontFamily = 'CALIBRI'; // changed from 'Arial, sans-serif'

        label.style.fontSize = '10px';
        // label.style.fontWeight = 'bold';
        label.style.color = 'white';
        
        // Create SVG toggle
        const svgNS = "http://www.w3.org/2000/svg";
        const svg = document.createElementNS(svgNS, "svg");
        // svg.setAttribute("width", "60");
        // svg.setAttribute("height", "30");
        svg.setAttribute("width", "30");
        svg.setAttribute("height", "10");
        svg.setAttribute("viewBox", "0 0 60 30");
        svg.style.cursor = "pointer";
        
        // Create toggle track
        const track = document.createElementNS(svgNS, "rect");
        track.setAttribute("x", "0");
        track.setAttribute("y", "0");
        track.setAttribute("rx", "15");
        track.setAttribute("ry", "15");
        track.setAttribute("width", "60");
        track.setAttribute("height", "30");
        track.setAttribute("fill", SORT_TOGGLE ? "#888" : "#ccc");
        
        // Create toggle circle/thumb
        const circle = document.createElementNS(svgNS, "circle");
        circle.setAttribute("cx", SORT_TOGGLE ? "45" : "15");
        circle.setAttribute("cy", "15");
        circle.setAttribute("r", "12");
        circle.setAttribute("fill", "white");
        
        // Add elements to SVG
        svg.appendChild(track);
        svg.appendChild(circle);
        
        // Add click event to SVG
        svg.addEventListener('click', function() {
            SORT_TOGGLE = !SORT_TOGGLE;
            track.setAttribute("fill", SORT_TOGGLE ? "#888" : "#ccc");
            circle.setAttribute("cx", SORT_TOGGLE ? "45" : "15");
            saveToggleState();
        });
        
        // Assemble button container
        buttonContainer.appendChild(label);
        buttonContainer.appendChild(svg);
        
        // Add container to document
        document.body.appendChild(buttonContainer);
        
        // Return references to elements that need to be updated
        return { track, circle };
    }
    
    // ---------- SAVE/LOAD TOGGLE STATE ----------
    function saveToggleState() {
        localStorage.setItem('sort-toggle-state', SORT_TOGGLE);
    }
    
    function loadToggleState() {
        const savedState = localStorage.getItem('sort-toggle-state');
        if (savedState !== null) {
            SORT_TOGGLE = savedState === 'true';
        }
    }
    
    // ---------- INITIALIZE ----------
    let svgElements = null;
    
    function initialize() {
        loadToggleState(); // Load state first
        svgElements = createToggleButton(); // Then create button with correct state
    }
    
    // Wait for the DOM to be fully loaded
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();

QingJ © 2025

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