YouTube Music: sort by play count

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

目前为 2025-03-27 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Music: sort by play count
  3. // @namespace https://github.com/KenKaneki73985
  4. // @match https://music.youtube.com/*
  5. // @version 1.0.1
  6. // @license MIT
  7. // @description Truly sort songs from an artist's page by play count from highest to lowest.
  8. // @author Ken Kaneki
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14. // ===== CONFIGURATION OPTIONS =====
  15. const NOTIFICATION_CONFIG = {
  16. // Positioning for "Sorting in Process" notification
  17. inProcessNotification: {
  18. top: '80%', // Vertical position (can use %, px, etc.)
  19. right: '38%', // Horizontal position (can use %, px, etc.)
  20. fontSize: '17px'
  21. },
  22. // Positioning for "Sorting Complete" notification
  23. sortingCompleteNotification: {
  24. top: '80%', // Different vertical position
  25. right: '45%', // Horizontal position
  26. fontSize: '17px'
  27. }
  28. };
  29. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  30. // Create a style for the notification
  31. const style = document.createElement('style');
  32. style.textContent = `
  33. #auto-dismiss-notification {
  34. position: fixed;
  35. color: white;
  36. padding: 15px;
  37. border-radius: 5px;
  38. z-index: 9999;
  39. transition: opacity 0.5s ease-out;
  40. }
  41. #auto-dismiss-notification.sorting-in-progress {
  42. background-color: rgba(0, 100, 0, 0.7); /* Green */
  43. }
  44. #auto-dismiss-notification.sorting-complete {
  45. background-color: rgba(82, 82, 255, 0.7); /* Blue */
  46. }`;
  47. document.head.appendChild(style);
  48.  
  49. let SORT_SONGS_BTN = document.createElement('button')
  50. 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>'
  51. SORT_SONGS_BTN.style.border = "none"
  52. SORT_SONGS_BTN.style.position = 'absolute'
  53. SORT_SONGS_BTN.style.left = '87%' // works in 125/150%
  54. SORT_SONGS_BTN.style.top = '2.5%'
  55. SORT_SONGS_BTN.style.padding = '0px'
  56. SORT_SONGS_BTN.style.background = "none"
  57. SORT_SONGS_BTN.style.zIndex = '9999'
  58. SORT_SONGS_BTN.addEventListener('click', () => {
  59. // Show message immediately
  60. MESSAGE_SORTING_IN_PROCESS();
  61. // Delay sorting to ensure message is visible
  62. setTimeout(() => {
  63. SORT_SONGS();
  64. // Show sorting complete message after sorting
  65. setTimeout(() => {
  66. MESSAGE_SORTING_COMPLETE();
  67. }, 500); // Small delay to ensure sorting is visually complete
  68. }, 50); // Small delay to ensure message rendering
  69. });
  70. document.body.appendChild(SORT_SONGS_BTN)
  71. }
  72. // Function to convert play count string to number
  73. function parsePlayCount(playString) {
  74. playString = playString.replace(' plays', '').trim();
  75. const multipliers = {
  76. 'B': 1000000000,
  77. 'M': 1000000,
  78. 'K': 1000
  79. };
  80. const match = playString.match(/^(\d+(?:\.\d+)?)\s*([BMK])?$/);
  81. if (!match) return 0;
  82. const number = parseFloat(match[1]);
  83. const multiplier = match[2] ? multipliers[match[2]] : 1;
  84. return number * multiplier;
  85. }
  86. function SORT_SONGS(){
  87. const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
  88. if (PLAYLIST_SHELF_DIV) {
  89. // Clone the original children to preserve event listeners
  90. const topLevelChildren = Array.from(PLAYLIST_SHELF_DIV.children);
  91. const songInfo = [];
  92. topLevelChildren.forEach((child, index) => {
  93. const titleElement = child.querySelector('div:nth-child(5) > div:nth-child(1) > yt-formatted-string:nth-child(1) > a:nth-child(1)');
  94. const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
  95. const songDetails = {
  96. element: child,
  97. id: `${index + 1}`,
  98. title: titleElement ? titleElement.textContent.trim() : 'Title not found',
  99. plays: playsElement ? playsElement.textContent.trim() : 'Plays not found',
  100. playCount: playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0
  101. };
  102. songInfo.push(songDetails);
  103. });
  104. // Sort songs by play count (highest to lowest)
  105. songInfo.sort((a, b) => b.playCount - a.playCount);
  106. // Use replaceChildren to preserve original event listeners
  107. PLAYLIST_SHELF_DIV.replaceChildren(...songInfo.map(song => song.element));
  108. // Modify song ranks without recreating elements
  109. songInfo.forEach((song, index) => {
  110. song.element.id = `${index + 1}`;
  111. });
  112. console.log("Success: Sorted By Play Count");
  113. } else {
  114. alert('error: Playlist shelf div not found');
  115. }
  116. }
  117.  
  118. function MESSAGE_SORTING_IN_PROCESS(){
  119. // Remove any existing notification
  120. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  121. if (EXISTING_NOTIFICATION) {
  122. EXISTING_NOTIFICATION.remove();
  123. }
  124.  
  125. // Create new notification element
  126. const notification = document.createElement('div');
  127. notification.id = 'auto-dismiss-notification';
  128. notification.classList.add('sorting-in-progress');
  129. notification.textContent = "Sorting in Process... Wait a few seconds"
  130.  
  131. // Apply configuration
  132. notification.style.top = NOTIFICATION_CONFIG.inProcessNotification.top;
  133. notification.style.right = NOTIFICATION_CONFIG.inProcessNotification.right;
  134. notification.style.fontSize = NOTIFICATION_CONFIG.inProcessNotification.fontSize;
  135.  
  136. // Append to body
  137. document.body.appendChild(notification);
  138.  
  139. // Auto-dismiss after 3 seconds
  140. setTimeout(() => {
  141. notification.style.opacity = '0';
  142. setTimeout(() => {
  143. notification.remove();
  144. }, 500); // matches transition time
  145. }, 3000);
  146. }
  147.  
  148. function MESSAGE_SORTING_COMPLETE(){
  149. // Remove any existing notification
  150. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  151. if (EXISTING_NOTIFICATION) {
  152. EXISTING_NOTIFICATION.remove();
  153. }
  154.  
  155. // Create new notification element
  156. const notification = document.createElement('div');
  157. notification.id = 'auto-dismiss-notification';
  158. notification.classList.add('sorting-complete');
  159. notification.textContent = "Sorting Complete"
  160.  
  161. // Apply configuration
  162. notification.style.top = NOTIFICATION_CONFIG.sortingCompleteNotification.top;
  163. notification.style.right = NOTIFICATION_CONFIG.sortingCompleteNotification.right;
  164. notification.style.fontSize = NOTIFICATION_CONFIG.sortingCompleteNotification.fontSize;
  165.  
  166. // Append to body
  167. document.body.appendChild(notification);
  168.  
  169. // Auto-dismiss after 3 seconds
  170. setTimeout(() => {
  171. notification.style.opacity = '0';
  172. setTimeout(() => {
  173. notification.remove();
  174. }, 500); // matches transition time
  175. }, 3000);
  176. }
  177. })();