RYM Track Rating Sorter

Sort RYM track ratings by score with a toggle button

  1. // ==UserScript==
  2. // @name RYM Track Rating Sorter
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @description Sort RYM track ratings by score with a toggle button
  6. // @author You
  7. // @match https://rateyourmusic.com/release/*
  8. // @icon https://www.google.com/s2/favicons?domain=rateyourmusic.com
  9. // @grant GM_addStyle
  10. // @grant GM_log
  11. // @run-at document-idle
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. GM_addStyle(`
  18. #rymSortBtn {
  19. all: unset !important;
  20. padding: 8px 15px !important;
  21. margin: 15px 0 !important;
  22. background: #4CAF50 !important;
  23. color: white !important;
  24. border-radius: 4px !important;
  25. font-size: 14px !important;
  26. cursor: pointer !important;
  27. position: relative !important;
  28. z-index: 9999 !important;
  29. font-family: Arial !important;
  30. display: block !important;
  31. }
  32. `);
  33.  
  34. function log(message) {
  35. console.log('[RYM Sorter]', message);
  36. }
  37.  
  38. function findTracklistContainer() {
  39. // Try multiple selectors in priority order
  40. const selectors = [
  41. 'div[data-testid="tracklist"]', // New RYM selector
  42. '.release_tracklist',
  43. '.tracklist_wrapper',
  44. '.tracklist',
  45. '.tracklist_list',
  46. '#tracks',
  47. 'div.tracks'
  48. ];
  49.  
  50. for (const selector of selectors) {
  51. const element = document.querySelector(selector);
  52. if (element) {
  53. log(`Found container using selector: ${selector}`);
  54. return element;
  55. }
  56. }
  57.  
  58. console.error('Tracklist container not found. Tried selectors:', selectors);
  59. return null;
  60. }
  61.  
  62. function initialize() {
  63. const container = findTracklistContainer();
  64. if (!container) {
  65. // Retry after short delay for dynamic content
  66. setTimeout(initialize, 1000);
  67. return;
  68. }
  69.  
  70. log('Initializing with container:', container);
  71.  
  72. // Create and insert button
  73. const btn = document.createElement('button');
  74. btn.id = 'rymSortBtn';
  75. btn.textContent = 'Sort by Rating ▼';
  76. container.parentNode.insertBefore(btn, container);
  77.  
  78. // Tracklist handling logic
  79. let originalHTML = container.innerHTML;
  80. let isSorted = false;
  81.  
  82. btn.addEventListener('click', () => {
  83. if (isSorted) {
  84. container.innerHTML = originalHTML;
  85. btn.textContent = 'Sort by Rating ▼';
  86. } else {
  87. const tracks = Array.from(container.querySelectorAll('.track, [data-testid="track"]'));
  88. tracks.sort((a, b) => {
  89. const getRating = el => {
  90. const ratingEl = el.querySelector('.track_rating, .track_rating_value, [class*="rating"]');
  91. return ratingEl ? parseFloat(ratingEl.textContent.replace('%', '')) || 0 : 0;
  92. };
  93. return getRating(b) - getRating(a);
  94. });
  95.  
  96. container.replaceChildren(...tracks);
  97. btn.textContent = 'Restore Original Order ▲';
  98. }
  99. isSorted = !isSorted;
  100. });
  101. }
  102.  
  103. // Start initialization with multiple fallbacks
  104. const init = () => {
  105. if (findTracklistContainer()) {
  106. initialize();
  107. } else {
  108. // Use MutationObserver as fallback
  109. const observer = new MutationObserver((mutations) => {
  110. if (findTracklistContainer()) {
  111. observer.disconnect();
  112. initialize();
  113. }
  114. });
  115. observer.observe(document.body, {
  116. childList: true,
  117. subtree: true
  118. });
  119.  
  120. // Final fallback timeout
  121. setTimeout(() => {
  122. if (!document.getElementById('rymSortBtn')) {
  123. initialize();
  124. }
  125. }, 3000);
  126. }
  127. };
  128.  
  129. if (document.readyState === 'complete') {
  130. init();
  131. } else {
  132. window.addEventListener('load', init);
  133. document.addEventListener('DOMContentLoaded', init);
  134. }
  135. })();