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