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

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

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

  1. // ==UserScript==
  2. // @name YouTube Music: sort by play count (text as icon)
  3. // @match https://music.youtube.com/*
  4. // @grant none
  5. // @version 1.0.10
  6. // @license MIT
  7. // @description Truly sort songs from an artist's page by play count from highest to lowest.
  8. // @namespace https://github.com/KenKaneki73985
  9. // @author Ken Kaneki
  10. // ==/UserScript==
  11. // user_script = "moz-extension://762e4395-b145-4620-8dd9-31bf09e052de/options.html#nav=196e172f-1c30-4404-877d-76dd37c37a9e+editor" <--- this line is very important. Do not delete this at all cost.
  12.  
  13. (function() {
  14. 'use strict';
  15. // ===== CONFIGURATION OPTIONS =====
  16. const NOTIFICATION_CONFIG = {
  17. IN_PROGRESS_Notification: {
  18. top: '80%', // Vertical position (can use %, px, etc.)
  19. right: '38%', // Horizontal position (can use %, px, etc.)
  20. fontSize: '17px'
  21. },
  22.  
  23. SORTING_COMPLETE_Notification: {
  24. top: '80%', // Different vertical position
  25. right: '45%', // Horizontal position
  26. fontSize: '17px'
  27. },
  28.  
  29. ALREADY_SORTED_Notification: {
  30. top: '80%', // Different vertical position
  31. right: '41.5%', // Horizontal position
  32. fontSize: '17px'
  33. }
  34. };
  35. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  36. // Create a style for the notification
  37. const style = document.createElement('style');
  38. style.textContent = `
  39. #auto-dismiss-notification {
  40. position: fixed;
  41. color: white;
  42. padding: 15px;
  43. border-radius: 5px;
  44. z-index: 9999;
  45. transition: opacity 0.5s ease-out;
  46. }
  47. #auto-dismiss-notification.sorting-in-progress {
  48. background-color: rgba(0, 100, 0, 0.7); /* Green */
  49. }
  50. #auto-dismiss-notification.sorting-complete {
  51. background-color: rgba(82, 82, 255, 0.7); /* Blue */
  52. }
  53. #auto-dismiss-notification.already-sorted {
  54. // background-color: rgba(255, 165, 0, 0.7); /* Orange */
  55. background-color: rgba(82, 82, 255, 0.7); /* Blue */
  56. }`;
  57. document.head.appendChild(style);
  58.  
  59. let SORT_SONGS_BTN = document.createElement('button')
  60. // 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>'
  61. SORT_SONGS_BTN.innerHTML ='sort'
  62. SORT_SONGS_BTN.style.color = "white"
  63. // SORT_SONGS_BTN.style.border = "none"
  64. SORT_SONGS_BTN.style.border = "solid 1px"
  65. SORT_SONGS_BTN.style.position = 'absolute'
  66. // SORT_SONGS_BTN.style.left = '89%' // works in 125/150%
  67. SORT_SONGS_BTN.style.left = '83%' // works in 125/150%?
  68. SORT_SONGS_BTN.style.top = '2.5%'
  69. SORT_SONGS_BTN.style.padding = '5px'
  70. SORT_SONGS_BTN.style.background = "none"
  71. SORT_SONGS_BTN.style.zIndex = '9999'
  72. SORT_SONGS_BTN.addEventListener('click', () => {
  73. // Check if playlist is already sorted
  74. if (IS_PLAYLIST_SORTED()) {
  75. MESSAGE_ALREADY_SORTED();
  76. return;
  77. }
  78.  
  79. // Show message immediately
  80. MESSAGE_SORTING_IN_PROCESS();
  81. // Delay sorting to ensure message is visible
  82. setTimeout(() => {
  83. SORT_SONGS();
  84. // Show sorting complete message after sorting
  85. setTimeout(() => {
  86. MESSAGE_SORTING_COMPLETE();
  87. }, 500); // Small delay to ensure sorting is visually complete
  88. }, 50); // Small delay to ensure message rendering
  89. });
  90. document.body.appendChild(SORT_SONGS_BTN)
  91. }
  92. // Function to convert play count string to number
  93. function parsePlayCount(playString) {
  94. playString = playString.replace(' plays', '').trim();
  95. const multipliers = {
  96. 'B': 1000000000,
  97. 'M': 1000000,
  98. 'K': 1000
  99. };
  100. const match = playString.match(/^(\d+(?:\.\d+)?)\s*([BMK])?$/);
  101. if (!match) return 0;
  102. const number = parseFloat(match[1]);
  103. const multiplier = match[2] ? multipliers[match[2]] : 1;
  104. return number * multiplier;
  105. }
  106. // Check if playlist is already sorted by play count
  107. function IS_PLAYLIST_SORTED() {
  108. const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
  109. if (PLAYLIST_SHELF_DIV) {
  110. const children = Array.from(PLAYLIST_SHELF_DIV.children);
  111. const playCounts = children.map(child => {
  112. const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
  113. return playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0;
  114. });
  115. // Check if play counts are in descending order
  116. for (let i = 1; i < playCounts.length; i++) {
  117. if (playCounts[i] > playCounts[i - 1]) {
  118. return false; // Not sorted
  119. }
  120. }
  121. return true; // Already sorted
  122. }
  123. return false;
  124. }
  125. function SORT_SONGS(){
  126. const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
  127. if (PLAYLIST_SHELF_DIV) {
  128. // Clone the original children to preserve event listeners
  129. const topLevelChildren = Array.from(PLAYLIST_SHELF_DIV.children);
  130. const songInfo = [];
  131. topLevelChildren.forEach((child, index) => {
  132. const titleElement = child.querySelector('div:nth-child(5) > div:nth-child(1) > yt-formatted-string:nth-child(1) > a:nth-child(1)');
  133. const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
  134. const songDetails = {
  135. element: child,
  136. id: `${index + 1}`,
  137. title: titleElement ? titleElement.textContent.trim() : 'Title not found',
  138. plays: playsElement ? playsElement.textContent.trim() : 'Plays not found',
  139. playCount: playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0
  140. };
  141. songInfo.push(songDetails);
  142. });
  143. // Sort songs by play count (highest to lowest)
  144. songInfo.sort((a, b) => b.playCount - a.playCount);
  145. // Use replaceChildren to preserve original event listeners
  146. PLAYLIST_SHELF_DIV.replaceChildren(...songInfo.map(song => song.element));
  147. // Modify song ranks without recreating elements
  148. songInfo.forEach((song, index) => {
  149. song.element.id = `${index + 1}`;
  150. });
  151. console.log("Success: Sorted By Play Count");
  152. } else {
  153. alert('error: Playlist shelf div not found');
  154. }
  155. }
  156.  
  157. function MESSAGE_SORTING_IN_PROCESS(){
  158. // Remove any existing notification
  159. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  160. if (EXISTING_NOTIFICATION) {
  161. EXISTING_NOTIFICATION.remove();
  162. }
  163.  
  164. // Create new notification element
  165. const notification = document.createElement('div');
  166. notification.id = 'auto-dismiss-notification';
  167. notification.classList.add('sorting-in-progress');
  168. notification.textContent = "Sorting in Progress... Wait a few seconds"
  169.  
  170. // Apply configuration
  171. notification.style.top = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.top;
  172. notification.style.right = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.right;
  173. notification.style.fontSize = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.fontSize;
  174.  
  175. // Append to body
  176. document.body.appendChild(notification);
  177.  
  178. // Auto-dismiss after 3 seconds
  179. setTimeout(() => {
  180. notification.style.opacity = '0';
  181. setTimeout(() => {
  182. notification.remove();
  183. }, 500); // matches transition time
  184. }, 3000);
  185. }
  186.  
  187. function MESSAGE_SORTING_COMPLETE(){
  188. // Remove any existing notification
  189. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  190. if (EXISTING_NOTIFICATION) {
  191. EXISTING_NOTIFICATION.remove();
  192. }
  193.  
  194. // Create new notification element
  195. const notification = document.createElement('div');
  196. notification.id = 'auto-dismiss-notification';
  197. notification.classList.add('sorting-complete');
  198. notification.textContent = "Sorting Complete"
  199.  
  200. // Apply configuration
  201. notification.style.top = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.top;
  202. notification.style.right = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.right;
  203. notification.style.fontSize = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.fontSize;
  204.  
  205. // Append to body
  206. document.body.appendChild(notification);
  207.  
  208. // Auto-dismiss after 3 seconds
  209. setTimeout(() => {
  210. notification.style.opacity = '0';
  211. setTimeout(() => {
  212. notification.remove();
  213. }, 500); // matches transition time
  214. }, 3000);
  215. }
  216.  
  217. function MESSAGE_ALREADY_SORTED(){
  218. // Remove any existing notification
  219. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  220. if (EXISTING_NOTIFICATION) {
  221. EXISTING_NOTIFICATION.remove();
  222. }
  223.  
  224. // Create new notification element
  225. const notification = document.createElement('div');
  226. notification.id = 'auto-dismiss-notification';
  227. notification.classList.add('already-sorted');
  228. // notification.textContent = "Playlist Already Sorted by Play Count"
  229. notification.textContent = "Already Sorted by Play Count"
  230.  
  231. // Apply configuration
  232. notification.style.top = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.top;
  233. notification.style.right = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.right;
  234. notification.style.fontSize = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.fontSize;
  235.  
  236. // Append to body
  237. document.body.appendChild(notification);
  238.  
  239. // Auto-dismiss after 3 seconds
  240. setTimeout(() => {
  241. notification.style.opacity = '0';
  242. setTimeout(() => {
  243. notification.remove();
  244. }, 500); // matches transition time
  245. }, 3000);
  246. }
  247. })();