// ==UserScript== // @name RYM Display Track Ratings // @namespace http://tampermonkey.net/ // @version 1.5 // @description Displays average Track ratings and info directly on rateyourmusic album or any other release pages. // @author cariosa // @match https://rateyourmusic.com/release/* // @icon https://e.snmc.io/2.5/img/sonemic.png // @grant GM_setValue // @grant GM_getValue // @license GPL-3.0-or-later // @downloadURL https://update.greasyfork.icu/scripts/527869/RYM%20Display%20Track%20Ratings.user.js // @updateURL https://update.greasyfork.icu/scripts/527869/RYM%20Display%20Track%20Ratings.meta.js // ==/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 loadClickCount = 0; // Count of how many times "Load Track Ratings" has been clicked let trackDataCache = getCache(); // Retrieve cache from storage or initialize empty object // Fetch the global state of the "Toggle Genre/Rankings" button let genreRankingsVisible = GM_getValue('genreRankingsVisible', true); // Logging function for debug messages const log = (message) => { if (DEBUG_MODE) { console.log(message); } }; // Show error notifications function showError(message) { const errorElement = document.createElement('div'); errorElement.textContent = `Error: ${message}`; errorElement.style.color = 'red'; errorElement.style.position = 'fixed'; errorElement.style.top = '10px'; errorElement.style.right = '10px'; errorElement.style.backgroundColor = 'white'; errorElement.style.padding = '5px'; document.body.appendChild(errorElement); setTimeout(() => errorElement.remove(), 5000); } // 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; } // Create buttons dynamically function createButtons(buttonsData) { const buttonContainer = document.createElement('div'); buttonContainer.style.marginBottom = '10px'; buttonsData.forEach(({ text, onClick }) => { const button = createButton(text, onClick); buttonContainer.appendChild(button); }); return buttonContainer; } // 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 = createButtons([ { text: 'Load Track Ratings', onClick: toggleTrackRatings }, { text: 'Toggle Genre/Rankings', onClick: toggleGenreRankings }, { text: 'Clear Cache', onClick: clearCache } ]); 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('
'); return { genre, rankings }; } // Create HTML for displaying the track rating function createRatingHTML(rating, count, isBold) { const starClass = isBold ? 'metadata-star-bold' : 'metadata-star'; return ` ${isBold ? 'bold star' : 'star'}
${rating}
${count}
`; } // 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) { if (!genreRankingsVisible) { return; } 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) { const songLink = trackElement.querySelector('a.song'); if (!songLink) { log(`No song link found for track ${index + 1}`); return; } const trackName = songLink.textContent.trim(); const cacheKey = `rym_track_data_${trackName}`; const cachedData = trackDataCache[cacheKey]; if (cachedData) { log(`Using cached data for "${trackName}"`); insertTrackRating(trackElement, cachedData.rating, cachedData.count, cachedData.isBold); insertTrackInfo(trackElement, cachedData.genre, cachedData.rankings); return; } try { log(`Fetching data for track: "${trackName}"`); const response = await fetch(songLink.href, { method: 'GET', credentials: 'include', }); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); const responseText = await response.text(); const trackRating = parseTrackRating(responseText); const trackInfo = parseTrackInfo(responseText); if (!trackRating || !trackInfo) { log(`Failed to fetch data for "${trackName}"`); return; } trackDataCache[cacheKey] = { ...trackRating, ...trackInfo, timestamp: Date.now() }; GM_setValue('trackDataCache', trackDataCache); insertTrackRating(trackElement, trackRating.rating, trackRating.count, trackRating.isBold); insertTrackInfo(trackElement, trackInfo.genre, trackInfo.rankings); } catch (error) { showError(`Failed to fetch data for "${trackName}". Please try again later.`); console.error(`Error processing "${trackName}":`, error); } await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY)); } // Process all tracks on the current page async function processAllTracks() { 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); } } } // Toggle the track ratings visibility and load/unload the data function toggleTrackRatings() { if (loadClickCount % 2 === 0) { log('Loading track ratings'); processAllTracks(); } else { log('Unloading track ratings'); clearTrackRatings(); } loadClickCount++; } // Clear the track ratings and additional data from the page function clearTrackRatings() { const ratingElements = document.querySelectorAll('.page_release_section_tracks_track_stats_scores'); ratingElements.forEach(el => el.remove()); const genreInfoElements = document.querySelectorAll('.genre-info'); genreInfoElements.forEach(el => el.remove()); const rankingInfoElements = document.querySelectorAll('.ranking-info'); rankingInfoElements.forEach(el => el.remove()); log('Cleared track ratings and additional info'); } // Toggle visibility of genre/rankings function toggleGenreRankings() { genreRankingsVisible = !genreRankingsVisible; GM_setValue('genreRankingsVisible', genreRankingsVisible); // Save state globally // Hide or show genre/rankings based on the state const genreInfoElements = document.querySelectorAll('.genre-info'); const rankingInfoElements = document.querySelectorAll('.ranking-info'); genreInfoElements.forEach(el => el.style.display = genreRankingsVisible ? 'block' : 'none'); rankingInfoElements.forEach(el => el.style.display = genreRankingsVisible ? 'block' : 'none'); log(genreRankingsVisible ? 'Genres and rankings visible' : 'Genres and rankings hidden'); } // Clear cached data function clearCache() { trackDataCache = {}; GM_setValue('trackDataCache', trackDataCache); log('Cache cleared'); } // Get the cache data from GM storage or initialize it function getCache() { const cachedData = GM_getValue('trackDataCache', {}); const now = Date.now(); // Delete expired cache entries Object.keys(cachedData).forEach(key => { const entry = cachedData[key]; if (now - entry.timestamp > CACHE_EXPIRATION) { delete cachedData[key]; } }); return cachedData; } // Initialize the script by inserting the control buttons insertButtons(); })();