-
-
Save dansleboby/b8dacd07ed09dfcd851f7f42f6594136 to your computer and use it in GitHub Desktop.
// ==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); | |
})(); |
You need to go on a detail page of a song https://suno.com/song/1a042042-b5d5-43e3-8b50-965d1ec0e303 and hard refresh ( I don't support the fake loading) it was also a glitch with the orginal script I just update it. You should see:
@dansleboby
I am not experienced in any of this, but have downloaded Tampermonkey and added the above code. When I do a hard reset nothing is happening. Can someone please help me figure this out?
@dansleboby I was able to get it to work, It is downloading the SRT file, but one word per line, Is there a way to change this so that it will export a line of the song at a time?
love this code, just build in a switch that you can also go for lines (not only words) when using .srt format.
When using lines (lyrics) commands like [Intro] [Outro] etc are removed.
You can go with this .srt file directly on youtube subtitles!
// ==UserScript==
// @name Suno Aligned Words Fetcher with Auth
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Fetch aligned words with auth and add a button under the image on Suno pages.
// @author dansleboby + Dschi
// @match https://suno.com/song/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const file_type = "srt"; // lrc ou srt
const use_lyrics = true;
// 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();
console.log("response json", data);
if (data && data.aligned_words) {
if(use_lyrics && file_type == "srt"){
console.log('aligned lyrics:', data.aligned_lyrics);
return data.aligned_lyrics;
}
else{
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`;
if(use_lyrics){
srtContent += `${wordObj.text.replace(/\[.*?\]/g, '')}\n\n`;
}
else{
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;
}
console.log("fetching song", songId);
// Fetch aligned words and add the button
fetchAlignedWords(songId, sessionToken).then((alignedWords) => {
if (alignedWords) {
addButton(imageSrcPattern, alignedWords);
}
});
}
console.log("start suno.ai checking");
setTimeout(function() { main(); }, 5000);
})();
[SOLVED]: Any chance to get the short guide on how to use it for newbies? It is not clear where should I get the bearer token. Suno doesn't have an official API afaik, however there are plenty of third party solutions online with questionable credibility. Probably community can share some services with good reputation though. The script that author shared looks amazing and the idea of sharing such tool for free is invaluable but there are some issues for beginners to solve.
Solution: Apologies for my ignorance. It turned out there is no need to use any third party services to obtain API keys, all you need to use is a browser extension called tampermonkey (you can find a link in the upmost part of of a code) and youhave to use the code inside this extension. Doublecheck if your browser allows the developer mode for extensions, tampermonkey provides comprehensive instructions on this matter.
Many thanks to the author - dansleboby for this gem he shared with us.
Install Tampermonkey
-
If you haven’t already, install the Tampermonkey browser extension for your browser:
Chrome: https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo
Firefox: https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/
Edge: https://microsoftedge.microsoft.com/addons/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo -
Open Tampermonkey Dashboard
Click the Tampermonkey icon in your browser and choose "Dashboard". -
Create a new script
In the dashboard, click the "+" (Create a new script) button in the top-left. -
Paste the code
Delete the default code and paste the full script from this Gist.
Save the script
Click File → Save, or press Ctrl + S.
Visit suno.ai and test
Open https://app.suno.ai/ (reload if already open). You should now see additional button to download lyrics — either word by word or per line, depending on your version.
The "bearer token" is used from your cookies see the part:
// Get the token from the cookie
const sessionToken = getCookie('__session');
if (!sessionToken) {
console.error('Session token not found in cookies.');
return;
}
so no need that you find that out, script will auto detect this. Also i didnt found any API documentation, just look into the response from the call and work with it :)
Install Tampermonkey
- If you haven’t already, install the Tampermonkey browser extension for your browser:
Chrome: https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo
Firefox: https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/
Edge: https://microsoftedge.microsoft.com/addons/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo- Open Tampermonkey Dashboard
Click the Tampermonkey icon in your browser and choose "Dashboard".- Create a new script
In the dashboard, click the "+" (Create a new script) button in the top-left.- Paste the code
Delete the default code and paste the full script from this Gist.
Save the script
Click File → Save, or press Ctrl + S.Visit suno.ai and test Open https://app.suno.ai/ (reload if already open). You should now see additional button to download lyrics — either word by word or per line, depending on your version.
The "bearer token" is used from your cookies see the part:
// Get the token from the cookie const sessionToken = getCookie('__session'); if (!sessionToken) { console.error('Session token not found in cookies.'); return; }
so no need that you find that out, script will auto detect this. Also i didnt found any API documentation, just look into the response from the call and work with it :)
@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!
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)
This is sick! But is it still working? I don't see the download button showing up.