Displays individual track ratings, genres, and track rankings on RateYourMusic album or any release pages.
// ==UserScript== // @name RYM Display Track Ratings // @namespace http://tampermonkey.net/ // @version 1.4 // @description Displays individual track ratings, genres, and track rankings on RateYourMusic album or any release pages. // @author cariosa // @match https://rateyourmusic.com/release/* // @icon https://e.snmc.io/2.5/img/sonemic.png // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @license GPL-3.0-or-later // ==/UserScript== (function() { 'use strict'; // Constants const CACHE_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // Cache expiration time in milliseconds (7 days) const DEBUG_MODE = false; // Enable debug mode for logging const DEFAULT_DELAY = 500; // Default delay between requests in milliseconds // Variables to manage state let ratingsVisible = false; // Track visibility state of ratings and additional info let loadClickCount = 0; // Count of how many times "Load Track Ratings" has been clicked let trackDataCache = GM_getValue('trackDataCache', {}); // Retrieve cache from storage or initialize empty object // Logging function for debug messages const log = (message) => { if (DEBUG_MODE) { console.log(message); } }; // Create a button with specified text and click handler function createButton(text, onClick) { const button = document.createElement('button'); button.textContent = text; button.style.cssText = ` margin-left: 3px; padding: 3px; border: 0; border-radius: 2px; background: #cba6f7; cursor: pointer; font-size: 10px; `; button.addEventListener('mouseover', () => button.style.backgroundColor = '#f2cdcd'); button.addEventListener('mouseout', () => button.style.backgroundColor = '#cba6f7'); button.addEventListener('click', onClick); return button; } // Insert control buttons for loading track ratings and genres/rankings function insertButtons() { const trackContainers = [ document.getElementById('tracks'), document.getElementById('tracks_mobile') ]; trackContainers.forEach((tracksContainer) => { if (!tracksContainer) { log('Tracks container not found'); return; } const buttonContainer = document.createElement('div'); buttonContainer.style.marginBottom = '10px'; // Create buttons for loading track ratings, toggling genre/rankings, and clearing cache const loadButton = createButton('Load Track Ratings', toggleTrackRatings); const toggleButton = createButton('Toggle Genre/Rankings', toggleGenreRankings); const clearCacheButton = createButton('Clear Cache', clearCache); buttonContainer.appendChild(loadButton); buttonContainer.appendChild(toggleButton); buttonContainer.appendChild(clearCacheButton); // Insert button container before the track list tracksContainer.parentNode.insertBefore(buttonContainer, tracksContainer); log('Buttons inserted successfully'); }); } // Parse the rating and count from the track's HTML function parseTrackRating(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const ratingElement = doc.querySelector('.page_section_main_info_music_rating_value_rating'); const countElement = doc.querySelector('.page_section_main_info_music_rating_value_number'); if (!ratingElement || !countElement) { log('Failed to find rating or count elements in HTML'); return null; } const rating = ratingElement.textContent.trim().match(/\d+\.\d+/)?.[0]; const count = countElement.textContent.trim().match(/[\d,]+/)?.[0]; const isBold = ratingElement.querySelector('img[alt="rating bolded"]') !== null; return { rating, count, isBold }; } // Parse genre and rankings from the track's HTML function parseTrackInfo(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const genreElement = doc.querySelector('.page_song_header_info_genre_item_primary .genre'); const genre = genreElement ? genreElement.outerHTML : null; const rankingElements = doc.querySelectorAll('.page_song_header_info_rest .comma_separated'); const rankings = Array.from(rankingElements).map(el => el.outerHTML).join('<br>'); return { genre, rankings }; } // Create HTML for displaying the track rating function createRatingHTML(rating, count, isBold) { const starClass = isBold ? 'metadata-star-bold' : 'metadata-star'; return ` <span data-tiptip="${rating} from ${count} ratings" class="has_tip page_release_section_tracks_songs_song_stats significant"> <span class="page_release_section_tracks_track_stats_scores"> <span class="page_release_section_tracks_track_stats_score_star pipe_separated"> <img alt="${isBold ? 'bold star' : 'star'}" class="${starClass}"> <div class="page_release_section_tracks_track_stats_rating pipe_separated"> ${rating} </div> </span> <div class="page_release_section_tracks_track_stats_count pipe_separated"> ${count} </div> </span> </span> `; } // Insert track rating HTML into the track element function insertTrackRating(trackElement, rating, count, isBold) { const tracklistLine = trackElement.querySelector('.tracklist_line'); const trackNumber = trackElement.querySelector('.tracklist_num'); if (tracklistLine && trackNumber) { const ratingElement = document.createElement('span'); ratingElement.innerHTML = createRatingHTML(rating, count, isBold); tracklistLine.insertBefore(ratingElement, trackNumber); log('Successfully inserted rating for track'); } } // Insert genre and rankings HTML into the track element function insertTrackInfo(trackElement, genre, rankings) { const tracklistLine = trackElement.querySelector('.tracklist_line'); if (tracklistLine) { const genreElement = document.createElement('div'); genreElement.innerHTML = genre; genreElement.style.marginTop = '2px'; genreElement.classList.add('genre-info'); tracklistLine.appendChild(genreElement); const rankingElement = document.createElement('div'); rankingElement.innerHTML = rankings; rankingElement.style.marginTop = '2px'; rankingElement.classList.add('ranking-info'); tracklistLine.appendChild(rankingElement); } } // Process the track data by fetching ratings and genre/rankings async function processTrackData(trackElement, index) { log(`Processing track ${index + 1}`); const songLink = trackElement.querySelector('a.song'); if (!songLink) { log(`No song link found for track ${index + 1}`); return; } const url = songLink.href; const trackName = songLink.textContent.trim(); const cacheKey = `rym_track_data_${trackName}`; const cachedData = trackDataCache[cacheKey]; if (cachedData) { const { rating, count, isBold, genre, rankings } = cachedData; log(`Using cached data for "${trackName}"`); insertTrackRating(trackElement, rating, count, isBold); insertTrackInfo(trackElement, genre, rankings); return; } try { log(`Fetching data for track: "${trackName}" from URL: ${url}`); const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: (response) => resolve(response), onerror: (error) => reject(error) }); }); const trackRating = parseTrackRating(response.responseText); const trackInfo = parseTrackInfo(response.responseText); if (!trackRating || !trackInfo) { log(`Failed to fetch data for "${trackName}"`); return; } // Cache the fetched track data trackDataCache[cacheKey] = { rating: trackRating.rating, count: trackRating.count, isBold: trackRating.isBold, genre: trackInfo.genre, rankings: trackInfo.rankings, }; GM_setValue('trackDataCache', trackDataCache); // Save cache to storage insertTrackRating(trackElement, trackRating.rating, trackRating.count, trackRating.isBold); insertTrackInfo(trackElement, trackInfo.genre, trackInfo.rankings); } catch (error) { console.error(`Error processing "${trackName}":`, error); alert(`Failed to fetch data for "${trackName}". Please try again later.`); } await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY)); // Delay between requests } // Process all tracks on the current page async function processAllTracks() { log('Starting to process tracks'); const trackContainers = [ document.getElementById('tracks'), document.getElementById('tracks_mobile') ]; for (const tracksContainer of trackContainers) { if (!tracksContainer) { log('Could not find tracks container'); return; } const tracks = tracksContainer.querySelectorAll('li.track'); log(`Found ${tracks.length} tracks`); for (let i = 0; i < tracks.length; i++) { await processTrackData(tracks[i], i); // Wait for each track to be processed } } log('Finished processing all tracks'); } // Toggle visibility of track ratings and additional info function toggleTrackRatings() { loadClickCount++; const tracksContainers = [ document.getElementById('tracks'), document.getElementById('tracks_mobile') ]; tracksContainers.forEach((tracksContainer) => { const trackRatings = tracksContainer.querySelectorAll('.page_release_section_tracks_songs_song_stats'); const genreElements = tracksContainer.querySelectorAll('.genre-info'); const rankingElements = tracksContainer.querySelectorAll('.ranking-info'); if (loadClickCount % 2 !== 0) { if (!ratingsVisible) { processAllTracks(); ratingsVisible = true; log('Track ratings and additional info loaded'); } } else { trackRatings.forEach(rating => rating.remove()); genreElements.forEach(genre => genre.remove()); rankingElements.forEach(ranking => ranking.remove()); ratingsVisible = false; log('Track ratings and additional info removed'); } }); } // Toggle visibility of genre and ranking information function toggleGenreRankings() { const genreElements = document.querySelectorAll('.genre-info'); const rankingElements = document.querySelectorAll('.ranking-info'); const isVisible = genreElements.length && genreElements[0].style.display !== 'none'; genreElements.forEach(el => el.style.display = isVisible ? 'none' : 'block'); rankingElements.forEach(el => el.style.display = isVisible ? 'none' : 'block'); } // Clear the track data cache function clearCache() { trackDataCache = {}; GM_setValue('trackDataCache', {}); // Clear cache from storage alert('Cache cleared!'); } // Initialize the script after the page is fully loaded window.addEventListener('load', () => { log('Page loaded, inserting buttons'); insertButtons(); }); })();