YouTube Music: sort by play count

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

当前为 2025-04-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Music: sort by play count
  3. // @match https://music.youtube.com/*
  4. // @grant none
  5. // @version 1.0.14
  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=d6bde39c-7fa5-41c7-bf85-3301be56dd30+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.fontSize = '12px';
  64. SORT_SONGS_BTN.style.border = "none"
  65. // SORT_SONGS_BTN.style.border = "solid 1px"
  66. SORT_SONGS_BTN.style.position = 'absolute'
  67.  
  68. // ---------- SVG ICON ----------
  69. SORT_SONGS_BTN.style.left = '83%' // works in 125/150%
  70. SORT_SONGS_BTN.style.top = '2%' // works in 125/150%
  71.  
  72. SORT_SONGS_BTN.style.padding = '5px'
  73. SORT_SONGS_BTN.style.background = "none"
  74. SORT_SONGS_BTN.style.zIndex = '9999'
  75. SORT_SONGS_BTN.addEventListener('click', () => {
  76. // Check if playlist is already sorted
  77. if (IS_PLAYLIST_SORTED()) {
  78. MESSAGE_ALREADY_SORTED();
  79. return;
  80. }
  81.  
  82. // Show message immediately
  83. MESSAGE_SORTING_IN_PROCESS();
  84. // Delay sorting to ensure message is visible
  85. setTimeout(() => {
  86. SORT_SONGS();
  87. // Show sorting complete message after sorting
  88. setTimeout(() => {
  89. MESSAGE_SORTING_COMPLETE();
  90. }, 500); // Small delay to ensure sorting is visually complete
  91. }, 50); // Small delay to ensure message rendering
  92. });
  93. document.body.appendChild(SORT_SONGS_BTN)
  94. }
  95. // Function to convert play count string to number
  96. function parsePlayCount(playString) {
  97. playString = playString.replace(' plays', '').trim();
  98. const multipliers = {
  99. 'B': 1000000000,
  100. 'M': 1000000,
  101. 'K': 1000
  102. };
  103. const match = playString.match(/^(\d+(?:\.\d+)?)\s*([BMK])?$/);
  104. if (!match) return 0;
  105. const number = parseFloat(match[1]);
  106. const multiplier = match[2] ? multipliers[match[2]] : 1;
  107. return number * multiplier;
  108. }
  109. // Check if playlist is already sorted by play count
  110. function IS_PLAYLIST_SORTED() {
  111. const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
  112. if (PLAYLIST_SHELF_DIV) {
  113. const children = Array.from(PLAYLIST_SHELF_DIV.children);
  114. const playCounts = children.map(child => {
  115. const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
  116. return playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0;
  117. });
  118. // Check if play counts are in descending order
  119. for (let i = 1; i < playCounts.length; i++) {
  120. if (playCounts[i] > playCounts[i - 1]) {
  121. return false; // Not sorted
  122. }
  123. }
  124. return true; // Already sorted
  125. }
  126. return false;
  127. }
  128. function SORT_SONGS(){
  129. const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
  130. if (PLAYLIST_SHELF_DIV) {
  131. // Clone the original children to preserve event listeners
  132. const topLevelChildren = Array.from(PLAYLIST_SHELF_DIV.children);
  133. const songInfo = [];
  134. topLevelChildren.forEach((child, index) => {
  135. const titleElement = child.querySelector('div:nth-child(5) > div:nth-child(1) > yt-formatted-string:nth-child(1) > a:nth-child(1)');
  136. const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
  137. const songDetails = {
  138. element: child,
  139. id: `${index + 1}`,
  140. title: titleElement ? titleElement.textContent.trim() : 'Title not found',
  141. plays: playsElement ? playsElement.textContent.trim() : 'Plays not found',
  142. playCount: playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0
  143. };
  144. songInfo.push(songDetails);
  145. });
  146. // Sort songs by play count (highest to lowest)
  147. songInfo.sort((a, b) => b.playCount - a.playCount);
  148. // Use replaceChildren to preserve original event listeners
  149. PLAYLIST_SHELF_DIV.replaceChildren(...songInfo.map(song => song.element));
  150. // Modify song ranks without recreating elements
  151. songInfo.forEach((song, index) => {
  152. song.element.id = `${index + 1}`;
  153. });
  154. console.log("Success: Sorted By Play Count");
  155. } else {
  156. console.log('error: Playlist shelf div not found');
  157.  
  158. }
  159. }
  160.  
  161. function MESSAGE_SORTING_IN_PROCESS(){
  162. // Remove any existing notification
  163. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  164. if (EXISTING_NOTIFICATION) {
  165. EXISTING_NOTIFICATION.remove();
  166. }
  167.  
  168. // Create new notification element
  169. const notification = document.createElement('div');
  170. notification.id = 'auto-dismiss-notification';
  171. notification.classList.add('sorting-in-progress');
  172. notification.textContent = "Sorting in Progress... Wait a few seconds"
  173.  
  174. // Apply configuration
  175. notification.style.top = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.top;
  176. notification.style.right = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.right;
  177. notification.style.fontSize = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.fontSize;
  178.  
  179. // Append to body
  180. document.body.appendChild(notification);
  181.  
  182. // Auto-dismiss after 3 seconds
  183. setTimeout(() => {
  184. notification.style.opacity = '0';
  185. setTimeout(() => {
  186. notification.remove();
  187. }, 500); // matches transition time
  188. }, 3000);
  189. }
  190.  
  191. function MESSAGE_SORTING_COMPLETE(){
  192. // Remove any existing notification
  193. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  194. if (EXISTING_NOTIFICATION) {
  195. EXISTING_NOTIFICATION.remove();
  196. }
  197.  
  198. // Create new notification element
  199. const notification = document.createElement('div');
  200. notification.id = 'auto-dismiss-notification';
  201. notification.classList.add('sorting-complete');
  202. notification.textContent = "Sorting Complete"
  203.  
  204. // Apply configuration
  205. notification.style.top = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.top;
  206. notification.style.right = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.right;
  207. notification.style.fontSize = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.fontSize;
  208.  
  209. // Append to body
  210. document.body.appendChild(notification);
  211.  
  212. // Auto-dismiss after 3 seconds
  213. setTimeout(() => {
  214. notification.style.opacity = '0';
  215. setTimeout(() => {
  216. notification.remove();
  217. }, 500); // matches transition time
  218. }, 3000);
  219. }
  220.  
  221. function MESSAGE_ALREADY_SORTED(){
  222. // Remove any existing notification
  223. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  224. if (EXISTING_NOTIFICATION) {
  225. EXISTING_NOTIFICATION.remove();
  226. }
  227.  
  228. // Create new notification element
  229. const notification = document.createElement('div');
  230. notification.id = 'auto-dismiss-notification';
  231. notification.classList.add('already-sorted');
  232. // notification.textContent = "Playlist Already Sorted by Play Count"
  233. notification.textContent = "Already Sorted by Play Count"
  234.  
  235. // Apply configuration
  236. notification.style.top = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.top;
  237. notification.style.right = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.right;
  238. notification.style.fontSize = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.fontSize;
  239.  
  240. // Append to body
  241. document.body.appendChild(notification);
  242.  
  243. // Auto-dismiss after 3 seconds
  244. setTimeout(() => {
  245. notification.style.opacity = '0';
  246. setTimeout(() => {
  247. notification.remove();
  248. }, 500); // matches transition time
  249. }, 3000);
  250. }
  251. })();