Last active
July 20, 2025 10:11
-
-
Save dansleboby/b8dacd07ed09dfcd851f7f42f6594136 to your computer and use it in GitHub Desktop.
Suno.com Aligned Words Fetcher to SRT or LRC file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Suno Aligned Words Fetcher with Auth | |
// @namespace http://tampermonkey.net/ | |
// @version 1.2 | |
// @description Fetch aligned words with auth and add a button under the image on Suno pages. | |
// @author Your Name | |
// @match https://suno.com/song/* | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
const file_type = "lrc"; // lrc ou srt | |
// Helper function to get the value of a cookie by name | |
function getCookie(name) { | |
const value = `; ${document.cookie}`; | |
const parts = value.split(`; ${name}=`); | |
if (parts.length === 2) return parts.pop().split(';').shift(); | |
} | |
// Helper function to fetch aligned words data with Bearer token | |
async function fetchAlignedWords(songId, token) { | |
const apiUrl = `https://studio-api.prod.suno.com/api/gen/${songId}/aligned_lyrics/v2/`; | |
try { | |
const response = await fetch(apiUrl, { | |
method: 'GET', | |
headers: { | |
'Authorization': `Bearer ${token}`, | |
'Content-Type': 'application/json' | |
} | |
}); | |
const data = await response.json(); | |
if (data && data.aligned_words) { | |
console.log('Aligned words:', data.aligned_words); | |
return data.aligned_words; | |
} else { | |
console.error('No aligned words found.'); | |
} | |
} catch (error) { | |
console.error('Error fetching aligned words:', error); | |
} | |
} | |
// Function to add a button under the image | |
function addButton(imageSrc, alignedWords) { | |
const imageElements = document.querySelectorAll(`img[src*="${imageSrc}"].w-full.h-full`); | |
console.log(imageSrc, imageElements); | |
imageElements.forEach(function(imageElement, k) { | |
console.log(k, imageElement); | |
if (imageElement) { | |
const button = document.createElement('button'); | |
button.innerText = 'Download '+file_type; | |
button.style.marginTop = '10px'; | |
button.style.zIndex = '9999'; | |
button.style.position = 'absolute'; | |
button.style.bottom = '0'; | |
button.style.left = '0'; | |
button.style.right = '0'; | |
button.style.background = 'gray'; | |
button.style.borderRadius = '5px'; | |
button.style.padding = '10px 6px'; | |
button.addEventListener('click', () => { | |
const srtContent = file_type === 'srt' ? convertToSRT(alignedWords) : convertToLRC(alignedWords); | |
const blob = new Blob([srtContent], { type: 'text/'+file_type }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = 'aligned_words.'+file_type; | |
a.click(); | |
URL.revokeObjectURL(url); // Clean up the URL object | |
}); | |
imageElement.parentNode.appendChild(button); | |
} else { | |
console.error('Image not found.'); | |
} | |
}); | |
} | |
// Function to convert aligned words to SRT format | |
function convertToSRT(alignedWords) { | |
let srtContent = ''; | |
alignedWords.forEach((wordObj, index) => { | |
const startTime = formatTime(wordObj.start_s); | |
const endTime = formatTime(wordObj.end_s); | |
srtContent += `${index + 1}\n`; | |
srtContent += `${startTime} --> ${endTime}\n`; | |
srtContent += `${wordObj.word}\n\n`; | |
}); | |
return srtContent; | |
} | |
// Helper function to format time into SRT format (HH:MM:SS,MS) | |
function formatTime(seconds) { | |
const date = new Date(0); | |
date.setMilliseconds(seconds * 1000); // Convert seconds to milliseconds | |
const hours = String(date.getUTCHours()).padStart(2, '0'); | |
const minutes = String(date.getUTCMinutes()).padStart(2, '0'); | |
const secs = String(date.getUTCSeconds()).padStart(2, '0'); | |
const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0'); | |
return `${hours}:${minutes}:${secs},${milliseconds}`; | |
} | |
// Function to convert aligned words to LRC format | |
function convertToLRC(alignedWords) { | |
let lrcContent = ''; | |
alignedWords.forEach(wordObj => { | |
const time = formatLrcTime(wordObj.start_s); | |
lrcContent += `${time}${wordObj.word}\n`; | |
}); | |
return lrcContent; | |
} | |
// Helper function to format time into LRC format [mm:ss.xx] | |
function formatLrcTime(seconds) { | |
const date = new Date(0); | |
date.setMilliseconds(seconds * 1000); // Convert seconds to milliseconds | |
const minutes = String(date.getUTCMinutes()).padStart(2, '0'); | |
const secs = String(date.getUTCSeconds()).padStart(2, '0'); | |
const hundredths = String(Math.floor(date.getUTCMilliseconds() / 10)).padStart(2, '0'); // Convert milliseconds to hundredths of a second | |
return `[${minutes}:${secs}.${hundredths}]`; | |
} | |
// Main function to run the script | |
function main() { | |
const urlParts = window.location.href.split('/'); | |
const songId = urlParts[urlParts.length - 1]; // Get song ID from URL | |
const imageSrcPattern = songId; | |
// Get the token from the cookie | |
const sessionToken = getCookie('__session'); | |
if (!sessionToken) { | |
console.error('Session token not found in cookies.'); | |
return; | |
} | |
// Fetch aligned words and add the button | |
fetchAlignedWords(songId, sessionToken).then((alignedWords) => { | |
if (alignedWords) { | |
addButton(imageSrcPattern, alignedWords); | |
} | |
}); | |
} | |
setTimeout(function() { main(); }, 5000); | |
})(); |
Where exactly should I be able to see the button?
i think this function no longer works as the get aligned lyrics api is no longer authorized...
its still authorized... they changed the structure of image URI thats why the button isn't created... here is the updated code to fix that:
// ==UserScript==
// @name Suno Aligned Words Fetcher with Auth & Cache
// @namespace http://tampermonkey.net/
// @version 1.6
// @description Fetch aligned words with auth, cache responses, and add a button under the image on Suno pages. Handles SPA URL changes globally.
// @author dansleboby + Dschi
// @match https://suno.com/*
// @match https://www.suno.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const file_type = "srt"; // lrc or srt
const use_lyrics = true;
const cache = {}; // Cache responses per songId
// Helper function to get the value of a cookie by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
async function fetchAlignedWords(songId, token) {
if (cache[songId]) {
console.log(`[Cache Hit] Using cached data for songId: ${songId}`);
return cache[songId];
}
const apiUrl = `https://studio-api.prod.suno.com/api/gen/${songId}/aligned_lyrics/v2/`;
try {
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log("[API Response]", data);
if (data && (data.aligned_words || data.aligned_lyrics)) {
const result = use_lyrics && file_type === "srt" ? data.aligned_lyrics : data.aligned_words;
cache[songId] = result; // Cache the result
return result;
} else {
console.error('No aligned words or lyrics found.');
}
} catch (error) {
console.error('Error fetching aligned words:', error);
}
}
function addButton(alignedWords) {
const imageElements = document.querySelectorAll('img.object-cover');
imageElements.forEach((imageElement) => {
// Remove existing button
const existingButton = imageElement.parentNode.querySelector('.my-suno-download-button');
if (existingButton) existingButton.remove();
const rect = imageElement.getBoundingClientRect();
if (rect.height > 100) {
const button = document.createElement('button');
button.innerText = `Download ${file_type}`;
button.className = 'my-suno-download-button';
button.style.marginTop = '10px';
button.style.zIndex = '9999';
button.style.position = 'absolute';
button.style.bottom = '0';
button.style.left = '0';
button.style.right = '0';
button.style.background = '#4CAF50';
button.style.color = '#fff';
button.style.border = 'none';
button.style.borderRadius = '5px';
button.style.padding = '10px 6px';
button.style.cursor = 'pointer';
button.addEventListener('click', () => {
const content = file_type === 'srt' ? convertToSRT(alignedWords) : convertToLRC(alignedWords);
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `aligned_words.${file_type}`;
a.click();
URL.revokeObjectURL(url);
});
imageElement.parentNode.appendChild(button);
}
});
}
function convertToSRT(alignedWords) {
let srtContent = '';
alignedWords.forEach((wordObj, index) => {
const startTime = formatTime(wordObj.start_s);
const endTime = formatTime(wordObj.end_s);
srtContent += `${index + 1}\n`;
srtContent += `${startTime} --> ${endTime}\n`;
srtContent += use_lyrics ? `${wordObj.text.replace(/\[.*?\]/g, '')}\n\n` : `${wordObj.word}\n\n`;
});
return srtContent;
}
function formatTime(seconds) {
const date = new Date(0);
date.setMilliseconds(seconds * 1000);
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const secs = String(date.getUTCSeconds()).padStart(2, '0');
const milliseconds = String(date.getUTCMilliseconds()).padStart(3, '0');
return `${hours}:${minutes}:${secs},${milliseconds}`;
}
function convertToLRC(alignedWords) {
let lrcContent = '';
alignedWords.forEach(wordObj => {
const time = formatLrcTime(wordObj.start_s);
lrcContent += `${time}${wordObj.word}\n`;
});
return lrcContent;
}
function formatLrcTime(seconds) {
const date = new Date(0);
date.setMilliseconds(seconds * 1000);
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
const secs = String(date.getUTCSeconds()).padStart(2, '0');
const hundredths = String(Math.floor(date.getUTCMilliseconds() / 10)).padStart(2, '0');
return `[${minutes}:${secs}.${hundredths}]`;
}
async function main() {
const url = window.location.href;
const match = url.match(/\/song\/([^/?#]+)/);
const songId = match ? match[1] : null;
if (!songId) {
console.log('[Suno Fetcher] Not a song page, skipping.');
return;
}
const sessionToken = getCookie('__session');
if (!sessionToken) {
console.error('Session token not found in cookies.');
return;
}
console.log(`[Suno Fetcher] Processing songId: ${songId}`);
const alignedWords = await fetchAlignedWords(songId, sessionToken);
if (alignedWords) {
addButton(alignedWords);
}
}
// Hook SPA navigation
(function(history) {
const pushState = history.pushState;
const replaceState = history.replaceState;
function fireUrlChange() {
const event = new Event('urlchange');
window.dispatchEvent(event);
}
history.pushState = function() {
const result = pushState.apply(this, arguments);
fireUrlChange();
return result;
};
history.replaceState = function() {
const result = replaceState.apply(this, arguments);
fireUrlChange();
return result;
};
window.addEventListener('popstate', fireUrlChange);
})(window.history);
// Watch for URL changes
window.addEventListener('urlchange', () => {
console.log('[Suno Fetcher] URL changed:', window.location.href);
setTimeout(main, 1500); // Delay for DOM
});
console.log('[Suno Fetcher] Initialized');
setTimeout(main, 1500);
})();
great script but right now still not working
great script but right now still not working
i edited again the script above (last post from yesterday) to fix a problem with new Parameters added to the URI ...
tested with this song:
https://suno.com/song/5924013e-5253-42a9-b478-87da3a0d0ed8?sh=oOrVwmQz77HZ7FLL
(after 5seconds you see the button at the cover image)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@dschibait, thanks for the prompt clarification and for your modified script which can export SRT for lines, I should confess, I have it in use already, great tool!