YouTube Music: sort by play count

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

目前为 2025-04-05 提交的版本。查看 最新版本

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