SunoAI-API / Suno-API

Stable and reliable, no deployment required, pay-as-you-go music generation API. 👇
https://app.foxai.me
MIT License
1.34k stars 181 forks source link

Automatic token refresh #39

Open RaynoldVanHeyningen opened 1 month ago

RaynoldVanHeyningen commented 1 month ago

I am running the Suno API properly, however i notice every 7 days, i have to update my session id and cookie value.

In the readme it says: "Automatic token maintenance and keep-alive" How can i make sure my token gets properly refreshed, without me having to manually update this information?

FergaliciousPixelicious commented 1 month ago

@RaynoldVanHeyningen in case you're interested, check out sumosuno.com

They are able to keep the token alive and it's a plug and play option in case you're interested

RaynoldVanHeyningen commented 3 weeks ago

@FergaliciousPixelicious Thanks, but this is not open-source, it's managed payware, so completely unrelated to this project.

I've built my own solution.

jtoy commented 3 weeks ago

@RaynoldVanHeyningen what is your solution?

RaynoldVanHeyningen commented 2 weeks ago

@RaynoldVanHeyningen what is your solution?

i noticed there was a form of token refresh going on, the main in problem 'in my situation' was that i was running the SunoAPI in a serverless environment, so the refreshed token wasn't persisted.

Since i'm running in Azure and using .NET for my application, i rebuild this Suno API solution into a custom .NET solution, hosted on Azure, with the main difference being that when a unauthorized request happens in the api, i refresh the session_id and cookie and store these in a redis cache. Afterwards the request will be executed again with the refreshed information.

Here is the relevant code, but as i said this is in .NET:

SunoCookieService.cs:

public class SunoCookieService : BackgroundService
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly IConfiguration _configuration;
    private readonly ILogger<SunoCookieService> _logger;
    private readonly IDistributedCache _cache;
    public SunoCookie SunoAuth { get; }
    private DateTime _tokenExpiryTime;

    public SunoCookieService(IHttpClientFactory clientFactory, IConfiguration configuration, ILogger<SunoCookieService> logger, IDistributedCache cache)
    {
        _clientFactory = clientFactory;
        _configuration = configuration;
        _logger = logger;
        _cache = cache;
        SunoAuth = new SunoCookie();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation($"------------------------------------------------------------------");
        _logger.LogInformation($"ExecuteAsync");

        await InitializeFromCache();

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                if (DateTime.UtcNow >= _tokenExpiryTime)
                {
                    await UpdateTokenAndSession();
                }
                else
                {
                    await TouchSession();
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error updating token or touching session");
            }

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }

    private async Task InitializeFromCache()
    {
        _logger.LogInformation($"------------------------------------------------------------------");
        _logger.LogInformation($"InitializeFromCache");

        var sessionId = await _cache.GetStringAsync("Suno:SessionId");
        var cookie = await _cache.GetStringAsync("Suno:Cookie");
        var token = await _cache.GetStringAsync("Suno:Token");
        var tokenExpiry = await _cache.GetStringAsync("Suno:TokenExpiry");

        if (string.IsNullOrEmpty(sessionId) || string.IsNullOrEmpty(cookie))
        {
            sessionId = _configuration["SunoAPI:SessionId"];
            cookie = _configuration["SunoAPI:Cookie"];

            if (string.IsNullOrEmpty(sessionId) || string.IsNullOrEmpty(cookie))
            {
                throw new InvalidOperationException("SessionId and Cookie must be provided either in cache or configuration.");
            }
            await _cache.SetStringAsync("Suno:SessionId", sessionId);
            await _cache.SetStringAsync("Suno:Cookie", cookie);
        }
        else
        {
            _logger.LogInformation("Initialized from cache");
        }

        SunoAuth.SetSessionId(sessionId);
        SunoAuth.LoadCookie(cookie);

        if (!string.IsNullOrEmpty(token))
        {
            SunoAuth.SetToken(token);
            if (DateTime.TryParse(tokenExpiry, out var expiry))
            {
                _tokenExpiryTime = expiry;
            }
            _logger.LogInformation($"Initialized with cached token: {token}, expiry time: {_tokenExpiryTime}");
        }
    }

    public async Task UpdateTokenAndSession()
    {
        _logger.LogInformation($"------------------------------------------------------------------");
        _logger.LogInformation($"UpdateTokenAndSession");

        var client = _clientFactory.CreateClient();
        client.DefaultRequestHeaders.Add("Cookie", SunoAuth.GetCookie());

        var response = await client.PostAsync(
            $"https://clerk.suno.com/v1/client/sessions/{SunoAuth.SessionId}/tokens?_clerk_js_version=4.72.0-snapshot.vc141245",
            null);

        if (!response.IsSuccessStatusCode)
        {
            _logger.LogWarning("Token update failed with status code {StatusCode}. Attempting to refresh session.", response.StatusCode);
            await RefreshSession(client);
            return;
        }

        var setCookie = response.Headers.GetValues("Set-Cookie").FirstOrDefault();
        if (!string.IsNullOrEmpty(setCookie))
        {
            SunoAuth.LoadCookie(setCookie);
            await _cache.SetStringAsync("Suno:Cookie", setCookie);
        }

        var responseContent = await response.Content.ReadAsStringAsync();
        var jsonResponse = JObject.Parse(responseContent);
        var token = jsonResponse["jwt"]?.ToString();
        var sessionId = ExtractSessionIdFromToken(token);

        if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(sessionId))
        {
            SunoAuth.SetToken(token);
            SunoAuth.SetSessionId(sessionId);
            await _cache.SetStringAsync("Suno:Token", token);
            await _cache.SetStringAsync("Suno:SessionId", sessionId);

            // Set token expiry time assuming token expiry time is 30 minutes from now
            _tokenExpiryTime = DateTime.UtcNow.AddMinutes(30); // Adjust according to actual token lifetime
            await _cache.SetStringAsync("Suno:TokenExpiry", _tokenExpiryTime.ToString());

            _logger.LogInformation($"Token updated successfully: {token}, new session ID: {sessionId}, new expiry time: {_tokenExpiryTime}");
        }
    }

    private async Task TouchSession()
    {
        _logger.LogInformation($"------------------------------------------------------------------");
        _logger.LogInformation($"TouchSession");

        var client = _clientFactory.CreateClient();
        client.DefaultRequestHeaders.Add("Cookie", SunoAuth.GetCookie());

        var response = await client.PostAsync(
            $"https://clerk.suno.com/v1/client/sessions/{SunoAuth.SessionId}/touch?_clerk_js_version=4.72.0-snapshot.vc141245",
            null);

        if (!response.IsSuccessStatusCode)
        {
            _logger.LogWarning("Touch session failed with status code {StatusCode}.", response.StatusCode);
            return;
        }

        var responseContent = await response.Content.ReadAsStringAsync();
        var jsonResponse = JObject.Parse(responseContent);
        var session = jsonResponse["response"]?["id"]?.ToString();

        if (!string.IsNullOrEmpty(session))
        {
            _logger.LogInformation($"Session touched successfully: {session}");
        }
    }

    private string ExtractSessionIdFromToken(string token)
    {
        _logger.LogInformation($"------------------------------------------------------------------");
        _logger.LogInformation($"ExtractSessionIdFromToken");

        if (string.IsNullOrEmpty(token)) return null;

        var parts = token.Split('.');
        if (parts.Length != 3) return null;

        var payload = parts[1];
        var json = Base64UrlDecode(payload);
        var jwtPayload = JObject.Parse(json);
        return jwtPayload["sid"]?.ToString();
    }

    private static string Base64UrlDecode(string input)
    {
        input = input.Replace('-', '+').Replace('_', '/');
        switch (input.Length % 4)
        {
            case 2: input += "=="; break;
            case 3: input += "="; break;
        }
        var bytes = Convert.FromBase64String(input);
        return Encoding.UTF8.GetString(bytes);
    }

    private async Task RefreshSession(HttpClient client)
    {
        _logger.LogInformation($"------------------------------------------------------------------");
        _logger.LogInformation($"RefreshSession");
        _logger.LogWarning("Session expired. Attempting to refresh...");

        var newSessionId = _configuration["SunoAPI:SessionId"];
        var newCookie = _configuration["SunoAPI:Cookie"];

        SunoAuth.SetSessionId(newSessionId);
        SunoAuth.LoadCookie(newCookie);

        await _cache.SetStringAsync("Suno:SessionId", newSessionId);
        await _cache.SetStringAsync("Suno:Cookie", newCookie);

        _logger.LogInformation("Session refreshed successfully");

        await UpdateTokenAndSession();  // Attempt to update the token again with the new session
    }
}