YouTube Music: sort by play count

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

  1. // ==UserScript==
  2. // @name YouTube Music: sort by play count
  3. // @match https://music.youtube.com/*
  4. // @grant none
  5. // @version 1.0.20
  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. document.addEventListener('click', () => {
  16.  
  17. if (SORT_TOGGLE){
  18. CLICK_SHOW_ALL_THEN_SORT()
  19. }
  20.  
  21. async function CLICK_SHOW_ALL_THEN_SORT() {
  22. await new Promise(resolve => setTimeout(resolve, 1000))
  23. // beware of "show all button" for possible location change
  24. 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)")
  25. if (SHOW_ALL_SONGS_BTN){
  26. SHOW_ALL_SONGS_BTN.click()
  27. MESSAGE_SORTING_IN_PROCESS()
  28.  
  29. await new Promise(resolve => setTimeout(resolve, 1000))
  30. SORT_SONGS()
  31. } else {
  32. // console.log("not found > 'show all songs' button. No sorting.");
  33. }
  34. }
  35. })
  36.  
  37. // ---------- CONFIGURATION OPTIONS ----------
  38. const NOTIFICATION_CONFIG = {
  39. IN_PROGRESS_Notification: {
  40. top: '82%', // Vertical position (can use %, px, etc.)
  41. // right: '38%', // "sorting in progress.. wait a few seconds"
  42. right: '44.5%', // "sorting in progress"
  43. fontSize: '16px',
  44. padding: '0px'
  45. },
  46.  
  47. SORTING_COMPLETE_Notification: {
  48. top: '82%', // Different vertical position
  49. right: '45%', // Horizontal position
  50. fontSize: '16px'
  51. },
  52.  
  53. ALREADY_SORTED_Notification: {
  54. // top: '85%',
  55. // right: '42.5%', // "already sorted by play count"
  56. // right: '46%', // "already sorted"
  57.  
  58. // ---------- TOP RIGHT ----------
  59. // top: '1.2%',
  60. // right: '17%',
  61.  
  62. // ---------- BOTTOM OF SORT ICON ----------
  63. top: '8%',
  64. right: '11%',
  65. fontSize: '13px'
  66. }
  67. };
  68.  
  69. // ---------------------- SORT BUTTON ----------------------
  70. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  71. // Create a style for the notification
  72. const style = document.createElement('style');
  73. style.textContent = `
  74. #auto-dismiss-notification {
  75. position: fixed;
  76. color: white;
  77. padding: 15px;
  78. border-radius: 5px;
  79. z-index: 9999;
  80. transition: opacity 0.5s ease-out;
  81. }
  82. #auto-dismiss-notification.sorting-in-progress {
  83. background-color: rgba(0, 100, 0, 0.7); /* Green */
  84. }
  85. #auto-dismiss-notification.sorting-complete {
  86. background-color: rgba(82, 82, 255, 0.7); /* Blue */
  87. }
  88. #auto-dismiss-notification.already-sorted {
  89. background-color: rgba(82, 82, 255, 0.7); /* Blue */
  90. // background-color: rgba(0, 0, 0, 0.7); /* Black */
  91. }`;
  92. document.head.appendChild(style);
  93.  
  94. let SORT_SONGS_BTN = document.createElement('button')
  95. 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>'
  96. SORT_SONGS_BTN.style.border = "none"
  97. // SORT_SONGS_BTN.style.position = 'absolute'
  98. SORT_SONGS_BTN.style.position = 'fixed'
  99. // SORT_SONGS_BTN.style.left = '89%' // works in 125/150%
  100. SORT_SONGS_BTN.style.left = '85.5%' // works in 125/150%?
  101. SORT_SONGS_BTN.style.top = '2.5%'
  102. SORT_SONGS_BTN.style.padding = '0px'
  103. SORT_SONGS_BTN.style.background = "none"
  104. SORT_SONGS_BTN.style.zIndex = '9999'
  105.  
  106. // ---------------------- SORT BUTTON CLICK EVENT ----------------------
  107. SORT_SONGS_BTN.addEventListener('click', () => {
  108. // Check if playlist is already sorted
  109. if (IS_PLAYLIST_SORTED()) {
  110. MESSAGE_ALREADY_SORTED();
  111. return;
  112. }
  113.  
  114. // IF NOT ALREADY SORTED
  115. MESSAGE_SORTING_IN_PROCESS();
  116.  
  117. CLICK_SHOW_ALL_THEN_SORT()
  118.  
  119. async function CLICK_SHOW_ALL_THEN_SORT() {
  120. await new Promise(resolve => setTimeout(resolve, 1000))
  121. 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)")
  122. if (SHOW_ALL_SONGS_BTN){
  123. SHOW_ALL_SONGS_BTN.click()
  124. MESSAGE_SORTING_IN_PROCESS()
  125.  
  126. await new Promise(resolve => setTimeout(resolve, 1000)) // <--- will "await new Promise" work fine here?
  127. SORT_SONGS()
  128. }
  129. else {
  130. SORT_SONGS()
  131. }
  132. }
  133. })
  134. document.body.appendChild(SORT_SONGS_BTN)
  135. }
  136. // Function to convert play count string to number
  137. function parsePlayCount(playString) {
  138. playString = playString.replace(' plays', '').trim();
  139. const multipliers = {
  140. 'B': 1000000000,
  141. 'M': 1000000,
  142. 'K': 1000
  143. };
  144. const match = playString.match(/^(\d+(?:\.\d+)?)\s*([BMK])?$/);
  145. if (!match) return 0;
  146. const number = parseFloat(match[1]);
  147. const multiplier = match[2] ? multipliers[match[2]] : 1;
  148. return number * multiplier;
  149. }
  150. // Check if playlist is already sorted by play count
  151. function IS_PLAYLIST_SORTED() {
  152. const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
  153. if (PLAYLIST_SHELF_DIV) {
  154. const children = Array.from(PLAYLIST_SHELF_DIV.children);
  155. const playCounts = children.map(child => {
  156. const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
  157. return playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0;
  158. });
  159. // Check if play counts are in descending order
  160. for (let i = 1; i < playCounts.length; i++) {
  161. if (playCounts[i] > playCounts[i - 1]) {
  162. return false; // Not sorted
  163. }
  164. }
  165. return true; // Already sorted
  166. }
  167. return false;
  168. }
  169.  
  170. function SORT_SONGS(){
  171. const PLAYLIST_SHELF_DIV = document.querySelector('div.ytmusic-playlist-shelf-renderer:nth-child(3)');
  172. if (PLAYLIST_SHELF_DIV) {
  173. // Clone the original children to preserve event listeners
  174. const topLevelChildren = Array.from(PLAYLIST_SHELF_DIV.children);
  175. const songInfo = [];
  176. topLevelChildren.forEach((child, index) => {
  177. const titleElement = child.querySelector('div:nth-child(5) > div:nth-child(1) > yt-formatted-string:nth-child(1) > a:nth-child(1)');
  178. const playsElement = child.querySelector('div:nth-child(5) > div:nth-child(3) > yt-formatted-string:nth-child(2)');
  179. const songDetails = {
  180. element: child,
  181. id: `${index + 1}`,
  182. title: titleElement ? titleElement.textContent.trim() : 'Title not found',
  183. plays: playsElement ? playsElement.textContent.trim() : 'Plays not found',
  184. playCount: playsElement ? parsePlayCount(playsElement.textContent.trim()) : 0
  185. };
  186. songInfo.push(songDetails);
  187. });
  188. // Sort songs by play count (highest to lowest)
  189. songInfo.sort((a, b) => b.playCount - a.playCount);
  190. // Use replaceChildren to preserve original event listeners
  191. PLAYLIST_SHELF_DIV.replaceChildren(...songInfo.map(song => song.element));
  192. // Modify song ranks without recreating elements
  193. songInfo.forEach((song, index) => {
  194. song.element.id = `${index + 1}`;
  195. });
  196. // console.log("Success: Sorted By Play Count");
  197. MESSAGE_SORTING_COMPLETE()
  198. } else {
  199. console.log("error: Playlist shelf div not found");
  200. // alert('error: Playlist shelf div not found');
  201. }
  202. }
  203.  
  204. function MESSAGE_SORTING_IN_PROCESS(){
  205. // Remove any existing notification
  206. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  207. if (EXISTING_NOTIFICATION) {
  208. EXISTING_NOTIFICATION.remove();
  209. }
  210.  
  211. // Create new notification element
  212. const notification = document.createElement('div');
  213. notification.id = 'auto-dismiss-notification';
  214. notification.classList.add('sorting-in-progress');
  215. // notification.textContent = "Sorting in Progress... Wait a few seconds"
  216. notification.textContent = "Sorting in Progress"
  217.  
  218. // Apply configuration
  219. notification.style.top = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.top;
  220. notification.style.right = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.right;
  221. notification.style.fontSize = NOTIFICATION_CONFIG.IN_PROGRESS_Notification.fontSize;
  222.  
  223. // Append to body
  224. document.body.appendChild(notification);
  225.  
  226. // Auto-dismiss after 3 seconds
  227. setTimeout(() => {
  228. notification.style.opacity = '0';
  229. setTimeout(() => {
  230. notification.remove();
  231. }, 500); // matches transition time
  232. }, 3000);
  233. }
  234.  
  235. function MESSAGE_SORTING_COMPLETE(){
  236. // Remove any existing notification
  237. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  238. if (EXISTING_NOTIFICATION) {
  239. EXISTING_NOTIFICATION.remove();
  240. }
  241.  
  242. // Create new notification element
  243. const notification = document.createElement('div');
  244. notification.id = 'auto-dismiss-notification';
  245. notification.classList.add('sorting-complete');
  246. notification.textContent = "Sorting Complete"
  247.  
  248. // Apply configuration
  249. notification.style.top = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.top;
  250. notification.style.right = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.right;
  251. notification.style.fontSize = NOTIFICATION_CONFIG.SORTING_COMPLETE_Notification.fontSize;
  252.  
  253. // Append to body
  254. document.body.appendChild(notification);
  255.  
  256. // Auto-dismiss after 3 seconds
  257. setTimeout(() => {
  258. notification.style.opacity = '0';
  259. setTimeout(() => {
  260. notification.remove();
  261. }, 500); // matches transition time
  262. }, 3000);
  263. }
  264.  
  265. function MESSAGE_ALREADY_SORTED(){
  266. // Remove any existing notification
  267. const EXISTING_NOTIFICATION = document.getElementById('auto-dismiss-notification');
  268. if (EXISTING_NOTIFICATION) {
  269. EXISTING_NOTIFICATION.remove();
  270. }
  271.  
  272. // Create new notification element
  273. const notification = document.createElement('div');
  274. notification.id = 'auto-dismiss-notification';
  275. notification.classList.add('already-sorted');
  276. // notification.textContent = "Playlist Already Sorted by Play Count"
  277. notification.textContent = "Already Sorted"
  278.  
  279. // Apply configuration
  280. notification.style.top = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.top;
  281. notification.style.right = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.right;
  282. notification.style.fontSize = NOTIFICATION_CONFIG.ALREADY_SORTED_Notification.fontSize;
  283.  
  284. // Append to body
  285. document.body.appendChild(notification);
  286.  
  287. // Auto-dismiss after 2 seconds
  288. setTimeout(() => {
  289. notification.style.opacity = '0';
  290. setTimeout(() => {
  291. notification.remove();
  292. }, 500); // matches transition time
  293. }, 1000);
  294. }
  295.  
  296. // ---------------------- TOGGLE BUTTON ----------------------
  297. // ---------- TOGGLE STATE ----------
  298. let SORT_TOGGLE = true;
  299. // ---------- CREATE TOGGLE BUTTON ----------
  300. function createToggleButton() {
  301. // Create button container
  302. const buttonContainer = document.createElement('div');
  303. buttonContainer.style.position = 'fixed';
  304. buttonContainer.style.top = '3.2%';
  305. buttonContainer.style.left = '76%';
  306. buttonContainer.style.zIndex = '9999';
  307. buttonContainer.style.backgroundColor = 'black';
  308. // buttonContainer.style.padding = '10px 15px';
  309. // buttonContainer.style.padding = '5px 10px';
  310. buttonContainer.style.padding = '3px 8px';
  311. buttonContainer.style.borderRadius = '8px';
  312. buttonContainer.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
  313. buttonContainer.style.display = 'flex';
  314. buttonContainer.style.alignItems = 'center';
  315. buttonContainer.style.gap = '0px';
  316. // Create label
  317. const label = document.createElement('span');
  318. label.textContent = 'AUTO SORT';
  319.  
  320. // ---------- FONT STYLE ----------
  321. // label.style.fontFamily = 'Arial, sans-serif';
  322. label.style.fontFamily = 'SEGOE UI'; // changed from 'Arial, sans-serif'
  323. // label.style.fontFamily = 'TAHOMA'; // changed from 'Arial, sans-serif'
  324. // label.style.fontFamily = 'CALIBRI'; // changed from 'Arial, sans-serif'
  325.  
  326. label.style.fontSize = '10px';
  327. // label.style.fontWeight = 'bold';
  328. label.style.color = 'white';
  329. // Create SVG toggle
  330. const svgNS = "http://www.w3.org/2000/svg";
  331. const svg = document.createElementNS(svgNS, "svg");
  332. // svg.setAttribute("width", "60");
  333. // svg.setAttribute("height", "30");
  334. svg.setAttribute("width", "30");
  335. svg.setAttribute("height", "10");
  336. svg.setAttribute("viewBox", "0 0 60 30");
  337. svg.style.cursor = "pointer";
  338. // Create toggle track
  339. const track = document.createElementNS(svgNS, "rect");
  340. track.setAttribute("x", "0");
  341. track.setAttribute("y", "0");
  342. track.setAttribute("rx", "15");
  343. track.setAttribute("ry", "15");
  344. track.setAttribute("width", "60");
  345. track.setAttribute("height", "30");
  346. track.setAttribute("fill", SORT_TOGGLE ? "#888" : "#ccc");
  347. // Create toggle circle/thumb
  348. const circle = document.createElementNS(svgNS, "circle");
  349. circle.setAttribute("cx", SORT_TOGGLE ? "45" : "15");
  350. circle.setAttribute("cy", "15");
  351. circle.setAttribute("r", "12");
  352. circle.setAttribute("fill", "white");
  353. // Add elements to SVG
  354. svg.appendChild(track);
  355. svg.appendChild(circle);
  356. // Add click event to SVG
  357. svg.addEventListener('click', function() {
  358. SORT_TOGGLE = !SORT_TOGGLE;
  359. track.setAttribute("fill", SORT_TOGGLE ? "#888" : "#ccc");
  360. circle.setAttribute("cx", SORT_TOGGLE ? "45" : "15");
  361. saveToggleState();
  362. });
  363. // Assemble button container
  364. buttonContainer.appendChild(label);
  365. buttonContainer.appendChild(svg);
  366. // Add container to document
  367. document.body.appendChild(buttonContainer);
  368. // Return references to elements that need to be updated
  369. return { track, circle };
  370. }
  371. // ---------- SAVE/LOAD TOGGLE STATE ----------
  372. function saveToggleState() {
  373. localStorage.setItem('sort-toggle-state', SORT_TOGGLE);
  374. }
  375. function loadToggleState() {
  376. const savedState = localStorage.getItem('sort-toggle-state');
  377. if (savedState !== null) {
  378. SORT_TOGGLE = savedState === 'true';
  379. }
  380. }
  381. // ---------- INITIALIZE ----------
  382. let svgElements = null;
  383. function initialize() {
  384. loadToggleState(); // Load state first
  385. svgElements = createToggleButton(); // Then create button with correct state
  386. }
  387. // Wait for the DOM to be fully loaded
  388. if (document.readyState === 'loading') {
  389. document.addEventListener('DOMContentLoaded', initialize);
  390. } else {
  391. initialize();
  392. }
  393. })();
  394.