Skip to content

Instantly share code, notes, and snippets.

@dansleboby
Last active January 27, 2026 15:53
Show Gist options
  • Select an option

  • Save dansleboby/b8dacd07ed09dfcd851f7f42f6594136 to your computer and use it in GitHub Desktop.

Select an option

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.3
// @description Fetch aligned words with auth and add a button under the image on Suno pages.
// @author Dschibait
// @match https://suno.com/song/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const file_type = "srt"; // lrc ou srt
// Helper function to get the value of a cookie by name and chooses the last item it there are duplicates
function getLastCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length > 1) {
return parts[parts.length - 1].split(';')[0];
}
}
// 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 = getLastCookie('__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

CellularDoor commented Jan 27, 2026

@Kinnar-V try this. This is slightly modified version of @dschibait code.

// ==UserScript==
// @name         Suno Aligned Words Fetcher with Auth & Cache
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Fetch aligned words with auth, cache responses, and add a button under the image on Suno pages. Handles SPA URL changes globally.
// @author       Dschibait
// @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 and chooses the last item it there are duplicates
    function getLastCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length > 1) {
        return parts[parts.length - 1].split(';')[0];
    }
    }

    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/`;

        let attempts = 0;
        const maxAttempts = 10;    // bis zu 10x neu versuchen
        const waitTime = 3000;     // 3 Sekunden Pause pro Versuch

        while (attempts < maxAttempts) {
            attempts++;

            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 Attempt ${attempts}]`, data);

                // Erfolgreich?
                if (data && (data.aligned_words || data.aligned_lyrics)) {
                    const result = use_lyrics && file_type === "srt"
                    ? data.aligned_lyrics
                    : data.aligned_words;

                    cache[songId] = result;
                    return result;
                }

                // Nicht verfügbar / Processing läuft noch
                if (data.detail && data.detail.includes("Processing")) {
                    console.warn(`[Suno] Lyrics not ready – new try in ${waitTime/1000}s...`);
                } else {
                    console.warn(`[Suno] no aligned words/lyrics – try again ${attempts}/${maxAttempts}`);
                }

            } catch (error) {
                console.error(`[Suno] error on API-Call (try ${attempts}):`, error);
            }

            // warten und erneut versuchen
            await new Promise(res => setTimeout(res, waitTime));
        }

        console.error("[Suno] maximum tries reached – cannot load lyrics.");
        return null;
    }

    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 = getLastCookie('__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);
})();

@Kinnar-V
Copy link

Kinnar-V commented Jan 27, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment