Shows the number of likes beside each music track on playlist pages on Suno.ai and allows sorting the playlist by likes.
// ==UserScript== // @name Suno Playlist Sorter // @namespace http://tampermonkey.net/ // @version 1.0 // @description Shows the number of likes beside each music track on playlist pages on Suno.ai and allows sorting the playlist by likes. // @author MahdeenSky // @match https://suno.com/playlist/*/ // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none // @license GNU GPLv3 // ==/UserScript== (function() { 'use strict'; const domain = "https://suno.com/"; const songLink_xPath = `//div//p//a[contains(@class, "chakra-link")]`; const likes_xPath = `//button[contains(@class, "chakra-button")]`; const playPlaylistButton_xPath = `//div[descendant::img[contains(@alt, "Playlist cover art")]]//div//div//button[contains(@class, "chakra-button")]`; let alreadyLikeFetched = {}; function setStyle(element, style) { for (let property in style) { element.style[property] = style[property]; } } function extractSongLink(songElement) { return songElement.getAttribute("href"); } function extractLikes(songLink) { return fetch(domain + songLink) .then(response => response.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const obfuscatedLikes = doc.evaluate(likes_xPath, doc, null, XPathR###lt.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; const likes = obfuscatedLikes.innerText.match(/;}(\d+)/)[1]; return likes; }) .catch(error => console.error(error)); } function addLikesToSongs() { const songSnapshots = document.evaluate(songLink_xPath, document, null, XPathR###lt.ORDERED_NODE_SNAPSHOT_TYPE, null); const promises = []; for (let i = 0; i < songSnapshots.snapshotLength; i++) { const songElement = songSnapshots.snapshotItem(i); const songLink = extractSongLink(songElement); if (alreadyLikeFetched[songLink]) { continue; } const promise = extractLikes(songLink).then(likes => { const likesElement = document.createElement("span"); likesElement.textContent = ` ${likes}`; fetchButtonDiv(songElement).then(buttonDiv => { buttonDiv.insertBefore(likesElement, buttonDiv.children[1]); alreadyLikeFetched[songLink] = likes; }); }); promises.push(promise); } return Promise.allSettled(promises); } function getKthParent(element, k) { let parent = element; for (let i = 0; i < k; i++) { parent = parent.parentNode; } return parent; } function fetchButtonDiv(songElement) { return Promise.resolve(getKthParent(songElement, 4).children[1].querySelector("div > button")); } function fetchLikesFromSongElement(songElement) { try { const likesElement = getKthParent(songElement, 4).children[1].querySelector("div > button").parentNode.querySelector("span"); return Promise.resolve(likesElement ? likesElement.textContent : null); } catch (error) { return Promise.resolve(null); } } function fetchSongGrid() { const songSnapshots = document.evaluate(songLink_xPath, document, null, XPathR###lt.ORDERED_NODE_SNAPSHOT_TYPE, null); const songElement = songSnapshots.snapshotItem(0); return Promise.resolve(getKthParent(songElement, 9)); } function sortSongsByLikes() { fetchSongGrid().then(songGrid => { let songRows = Array.from(songGrid.children); let songSnapshots = document.evaluate(songLink_xPath, document, null, XPathR###lt.ORDERED_NODE_SNAPSHOT_TYPE, null); let songElements = []; for (let i = 0; i < songSnapshots.snapshotLength; i++) { let songElement = songSnapshots.snapshotItem(i); songElements.push(songElement); } // check if the likes are already fetched, if not fetch them fetchLikesFromSongElement(songElements[songElements.length - 1]).then(likes => { if (likes === null) { addLikesToSongs().then(() => { sortSongsByLikes(); }); } else { let songElementsWithLikes = []; let promises = []; for (let i = 0; i < songElements.length; i++) { let songElement = songElements[i]; let promise = fetchLikesFromSongElement(songElement).then(likes => { songElementsWithLikes.push({ songElement: songElement, songRow: songRows[i], likes: likes }); }); promises.push(promise); } Promise.all(promises).then(() => { // All promises have resolved, songElementsWithLikes is now fully populated // sort the songElementsWithLikes array by likes songElementsWithLikes.sort((a, b) => { return parseInt(b.likes) - parseInt(a.likes); }); // replace each songRow with the sorted songRow // make a clone of the songGrid without the children let songGridClone = songGrid.cloneNode(false); for (let i = 0; i < songElementsWithLikes.length; i++) { songGridClone.appendChild(songElementsWithLikes[i].songRow); } // replace the songGrid with the sorted songGrid songGrid.replaceWith(songGridClone); }); } }); }); } function addSortButton() { const button = document.createElement("button"); button.textContent = "Sort by Likes"; button.onclick = sortSongsByLikes; setStyle(button, { backgroundColor: "#4CAF50", // Green background border: "none", color: "white", padding: "10px 24px", textAlign: "center", textDecoration: "none", display: "inline-block", fontSize: "16px", margin: "4px 2px", cursor: "pointer", borderRadius: "8px", // Rounded corners boxShadow: "0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19)" // Add a shadow }); const playlistPlayButton = document.evaluate(playPlaylistButton_xPath, document, null, XPathR###lt.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; playlistPlayButton.parentNode.appendChild(button); } function addLikesButton() { const button = document.createElement("button"); button.textContent = "Show Likes"; button.onclick = addLikesToSongs; setStyle(button, { backgroundColor: "#008CBA", // Blue background border: "none", color: "white", padding: "10px 24px", textAlign: "center", textDecoration: "none", display: "inline-block", fontSize: "16px", margin: "4px 2px", cursor: "pointer", borderRadius: "8px", // Rounded corners boxShadow: "0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19)" // Add a shadow }); let playlistPlayButton = document.evaluate(playPlaylistButton_xPath, document, null, XPathR###lt.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; playlistPlayButton.parentNode.appendChild(button); } // add button when the playlistPlayButton is loaded let observer = new MutationObserver((mutations, observer) => { let playlistPlayButton = document.evaluate(playPlaylistButton_xPath, document, null, XPathR###lt.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (playlistPlayButton) { addLikesButton(); addSortButton(); observer.disconnect(); } }); observer.observe(document.body, {childList: true, subtree: true}); })();