fix: add rate limiting for Odesli/song.link API

- Implemented semaphore-based rate limiter (10 requests per minute)
- Odesli API allows 10 requests per 60 seconds
- Rate limiter ensures 1 request per 6 seconds maximum
- Prevents API rate limit violations
- Cache still used first (30 day TTL) to minimize API calls
This commit is contained in:
2026-02-06 19:55:16 -05:00
parent 960d15175e
commit b366a4b771

View File

@@ -4471,54 +4471,70 @@ public class JellyfinController : ControllerBase
return cachedSpotifyId;
}
// Call Odesli API
var odesliUrl = $"https://api.song.link/v1-alpha.1/links?url={Uri.EscapeDataString(sourceUrl)}&userCountry=US";
// RATE LIMITING: Odesli allows 10 requests per minute
// Use a simple semaphore-based rate limiter
await OdesliRateLimiter.WaitAsync();
_logger.LogDebug("Calling Odesli API: {Url}", odesliUrl);
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(5);
var response = await httpClient.GetAsync(odesliUrl);
if (!response.IsSuccessStatusCode)
try
{
_logger.LogDebug("Odesli API returned {StatusCode} for {Provider}/{ExternalId}",
response.StatusCode, provider, externalId);
return null;
}
// Call Odesli API
var odesliUrl = $"https://api.song.link/v1-alpha.1/links?url={Uri.EscapeDataString(sourceUrl)}&userCountry=US";
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
_logger.LogDebug("Calling Odesli API: {Url}", odesliUrl);
// Extract Spotify URL from linksByPlatform.spotify.url
if (root.TryGetProperty("linksByPlatform", out var platforms) &&
platforms.TryGetProperty("spotify", out var spotify) &&
spotify.TryGetProperty("url", out var spotifyUrlEl))
{
var spotifyUrl = spotifyUrlEl.GetString();
if (!string.IsNullOrEmpty(spotifyUrl))
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(5);
var response = await httpClient.GetAsync(odesliUrl);
if (!response.IsSuccessStatusCode)
{
// Extract Spotify ID from URL: https://open.spotify.com/track/{id}
var match = System.Text.RegularExpressions.Regex.Match(spotifyUrl, @"spotify\.com/track/([a-zA-Z0-9]+)");
if (match.Success)
_logger.LogDebug("Odesli API returned {StatusCode} for {Provider}/{ExternalId}",
response.StatusCode, provider, externalId);
return null;
}
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Extract Spotify URL from linksByPlatform.spotify.url
if (root.TryGetProperty("linksByPlatform", out var platforms) &&
platforms.TryGetProperty("spotify", out var spotify) &&
spotify.TryGetProperty("url", out var spotifyUrlEl))
{
var spotifyUrl = spotifyUrlEl.GetString();
if (!string.IsNullOrEmpty(spotifyUrl))
{
var spotifyId = match.Groups[1].Value;
// Extract Spotify ID from URL: https://open.spotify.com/track/{id}
var match = System.Text.RegularExpressions.Regex.Match(spotifyUrl, @"spotify\.com/track/([a-zA-Z0-9]+)");
if (match.Success)
{
var spotifyId = match.Groups[1].Value;
// Cache the result (30 days)
await _cache.SetStringAsync(cacheKey, spotifyId, TimeSpan.FromDays(30));
// Cache the result (30 days)
await _cache.SetStringAsync(cacheKey, spotifyId, TimeSpan.FromDays(30));
_logger.LogInformation("✓ Odesli converted {Provider}/{ExternalId} → Spotify ID {SpotifyId}",
provider, externalId, spotifyId);
_logger.LogInformation("✓ Odesli converted {Provider}/{ExternalId} → Spotify ID {SpotifyId}",
provider, externalId, spotifyId);
return spotifyId;
return spotifyId;
}
}
}
}
_logger.LogDebug("No Spotify link found in Odesli response for {Provider}/{ExternalId}", provider, externalId);
return null;
_logger.LogDebug("No Spotify link found in Odesli response for {Provider}/{ExternalId}", provider, externalId);
return null;
}
finally
{
// Release rate limiter after 6 seconds (10 requests per 60 seconds = 1 request per 6 seconds)
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(6));
OdesliRateLimiter.Release();
});
}
}
catch (Exception ex)
{
@@ -4526,6 +4542,9 @@ public class JellyfinController : ControllerBase
return null;
}
}
// Static rate limiter for Odesli API (10 requests per minute = 1 request per 6 seconds)
private static readonly SemaphoreSlim OdesliRateLimiter = new SemaphoreSlim(10, 10);
}
// force rebuild Sun Jan 25 13:22:47 EST 2026