Skip to content

Instantly share code, notes, and snippets.

@dansleboby
Last active July 20, 2025 10:11
Show Gist options
  • Save dansleboby/b8dacd07ed09dfcd851f7f42f6594136 to your computer and use it in GitHub Desktop.
Save dansleboby/b8dacd07ed09dfcd851f7f42f6594136 to your computer and use it in GitHub Desktop.
Suno.com Aligned Words Fetcher to SRT or LRC file
// ==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);
})();
@CellularDoor
Copy link

Install Tampermonkey

  1. 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
  2. Open Tampermonkey Dashboard
    Click the Tampermonkey icon in your browser and choose "Dashboard".
  3. Create a new script
    In the dashboard, click the "+" (Create a new script) button in the top-left.
  4. 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!

@AliBassam
Copy link

Where exactly should I be able to see the button?

@dschibait
Copy link

dschibait commented Jun 18, 2025

Where exactly should I be able to see the button?

on a song overview page ... so when clicking on the name of a song, where you see the cover and lyrics (default), the button is grey and inside the cover-image.

Bildschirmfoto 2025-06-18 um 09 03 45

@ciaovention
Copy link

i think this function no longer works as the get aligned lyrics api is no longer authorized...

@dschibait
Copy link

dschibait commented Jul 16, 2025

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);
})();

@btitkin
Copy link

btitkin commented Jul 16, 2025

great script but right now still not working

@dschibait
Copy link

dschibait commented Jul 17, 2025

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