您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } })();