- // ==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();
- }
- })();
-