mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Fix AdminController Spotify 429 rate limiting in Link Playlists tab
- Replace direct REST API calls with SpotifyApiClient GraphQL method - Add GetUserPlaylistsAsync() method to fetch all user playlists via GraphQL - GraphQL endpoint is much less rate-limited than REST API /me/playlists - Enhanced playlist data with track count, owner, and image URL from GraphQL - Simplified AdminController code by delegating to SpotifyApiClient
This commit is contained in:
@@ -1939,12 +1939,6 @@ public class AdminController : ControllerBase
|
||||
|
||||
try
|
||||
{
|
||||
var token = await _spotifyClient.GetWebAccessTokenAsync();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return StatusCode(401, new { error = "Failed to authenticate with Spotify. Check your sp_dc cookie." });
|
||||
}
|
||||
|
||||
// Get list of already-configured Spotify playlist IDs
|
||||
var configuredPlaylists = await ReadPlaylistsFromEnvFile();
|
||||
var linkedSpotifyIds = new HashSet<string>(
|
||||
@@ -1952,81 +1946,23 @@ public class AdminController : ControllerBase
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
|
||||
var playlists = new List<object>();
|
||||
var offset = 0;
|
||||
const int limit = 50;
|
||||
// Use SpotifyApiClient's GraphQL method - much less rate-limited than REST API
|
||||
var spotifyPlaylists = await _spotifyClient.GetUserPlaylistsAsync(searchName: null);
|
||||
|
||||
while (true)
|
||||
if (spotifyPlaylists == null || spotifyPlaylists.Count == 0)
|
||||
{
|
||||
var url = $"https://api.spotify.com/v1/me/playlists?offset={offset}&limit={limit}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _jellyfinHttpClient.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_logger.LogWarning("Spotify rate limit hit (429) when fetching playlists");
|
||||
return StatusCode(429, new { error = "Spotify rate limit exceeded. Please wait a moment and try again." });
|
||||
return Ok(new { playlists = new List<object>() });
|
||||
}
|
||||
|
||||
_logger.LogWarning("Failed to fetch Spotify playlists: {StatusCode}", response.StatusCode);
|
||||
break;
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("items", out var items) || items.GetArrayLength() == 0)
|
||||
break;
|
||||
|
||||
foreach (var item in items.EnumerateArray())
|
||||
var playlists = spotifyPlaylists.Select(p => new
|
||||
{
|
||||
var id = item.TryGetProperty("id", out var itemId) ? itemId.GetString() : null;
|
||||
var name = item.TryGetProperty("name", out var n) ? n.GetString() : null;
|
||||
var trackCount = 0;
|
||||
|
||||
if (item.TryGetProperty("tracks", out var tracks) &&
|
||||
tracks.TryGetProperty("total", out var total))
|
||||
{
|
||||
trackCount = total.GetInt32();
|
||||
}
|
||||
|
||||
var owner = "";
|
||||
if (item.TryGetProperty("owner", out var ownerObj) &&
|
||||
ownerObj.TryGetProperty("display_name", out var displayName))
|
||||
{
|
||||
owner = displayName.GetString() ?? "";
|
||||
}
|
||||
|
||||
var isPublic = item.TryGetProperty("public", out var pub) && pub.GetBoolean();
|
||||
|
||||
// Check if this playlist is already linked
|
||||
var isLinked = !string.IsNullOrEmpty(id) && linkedSpotifyIds.Contains(id);
|
||||
|
||||
playlists.Add(new
|
||||
{
|
||||
id,
|
||||
name,
|
||||
trackCount,
|
||||
owner,
|
||||
isPublic,
|
||||
isLinked
|
||||
});
|
||||
}
|
||||
|
||||
if (items.GetArrayLength() < limit) break;
|
||||
offset += limit;
|
||||
|
||||
// Rate limiting
|
||||
if (_spotifyApiSettings.RateLimitDelayMs > 0)
|
||||
{
|
||||
await Task.Delay(_spotifyApiSettings.RateLimitDelayMs);
|
||||
}
|
||||
}
|
||||
id = p.SpotifyId,
|
||||
name = p.Name,
|
||||
trackCount = p.TotalTracks,
|
||||
owner = p.OwnerName ?? "",
|
||||
isPublic = p.Public,
|
||||
isLinked = linkedSpotifyIds.Contains(p.SpotifyId)
|
||||
}).ToList();
|
||||
|
||||
return Ok(new { playlists });
|
||||
}
|
||||
|
||||
@@ -746,6 +746,18 @@ public class SpotifyApiClient : IDisposable
|
||||
public async Task<List<SpotifyPlaylist>> SearchUserPlaylistsAsync(
|
||||
string searchName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetUserPlaylistsAsync(searchName, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all playlists from the user's library, optionally filtered by name.
|
||||
/// Uses GraphQL API which is less rate-limited than REST API.
|
||||
/// </summary>
|
||||
/// <param name="searchName">Optional name filter (case-insensitive). If null, returns all playlists.</param>
|
||||
public async Task<List<SpotifyPlaylist>> GetUserPlaylistsAsync(
|
||||
string? searchName = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var token = await GetWebAccessTokenAsync(cancellationToken);
|
||||
if (string.IsNullOrEmpty(token))
|
||||
@@ -854,22 +866,62 @@ public class SpotifyApiClient : IDisposable
|
||||
|
||||
var itemName = playlist.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
|
||||
|
||||
// Check if name matches (case-insensitive)
|
||||
if (itemName.Contains(searchName, StringComparison.OrdinalIgnoreCase))
|
||||
// Check if name matches (case-insensitive) - if searchName is provided
|
||||
if (!string.IsNullOrEmpty(searchName) &&
|
||||
!itemName.Contains(searchName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var uri = playlist.TryGetProperty("uri", out var u) ? u.GetString() ?? "" : "";
|
||||
var spotifyId = uri.Replace("spotify:playlist:", "", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Get track count if available
|
||||
var trackCount = 0;
|
||||
if (playlist.TryGetProperty("content", out var content) &&
|
||||
content.TryGetProperty("totalCount", out var totalCount))
|
||||
{
|
||||
trackCount = totalCount.GetInt32();
|
||||
}
|
||||
|
||||
// Get owner name
|
||||
string? ownerName = null;
|
||||
if (playlist.TryGetProperty("ownerV2", out var ownerV2) &&
|
||||
ownerV2.TryGetProperty("data", out var ownerData) &&
|
||||
ownerData.TryGetProperty("name", out var ownerNameProp))
|
||||
{
|
||||
ownerName = ownerNameProp.GetString();
|
||||
}
|
||||
|
||||
// Get image URL
|
||||
string? imageUrl = null;
|
||||
if (playlist.TryGetProperty("images", out var images) &&
|
||||
images.TryGetProperty("items", out var imageItems) &&
|
||||
imageItems.GetArrayLength() > 0)
|
||||
{
|
||||
var firstImage = imageItems[0];
|
||||
if (firstImage.TryGetProperty("sources", out var sources) &&
|
||||
sources.GetArrayLength() > 0)
|
||||
{
|
||||
var firstSource = sources[0];
|
||||
if (firstSource.TryGetProperty("url", out var urlProp))
|
||||
{
|
||||
imageUrl = urlProp.GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playlists.Add(new SpotifyPlaylist
|
||||
{
|
||||
SpotifyId = spotifyId,
|
||||
Name = itemName,
|
||||
Description = playlist.TryGetProperty("description", out var desc) ? desc.GetString() : null,
|
||||
TotalTracks = 0, // GraphQL doesn't return track count in this query
|
||||
TotalTracks = trackCount,
|
||||
OwnerName = ownerName,
|
||||
ImageUrl = imageUrl,
|
||||
SnapshotId = null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (itemCount < limit) break;
|
||||
offset += limit;
|
||||
@@ -881,12 +933,15 @@ public class SpotifyApiClient : IDisposable
|
||||
await Task.Delay(delayMs, cancellationToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Found {Count} playlists matching '{SearchName}' via GraphQL", playlists.Count, searchName);
|
||||
_logger.LogInformation("Found {Count} playlists{Filter} via GraphQL",
|
||||
playlists.Count,
|
||||
string.IsNullOrEmpty(searchName) ? "" : $" matching '{searchName}'");
|
||||
return playlists;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error searching user playlists for '{SearchName}' via GraphQL", searchName);
|
||||
_logger.LogError(ex, "Error fetching user playlists{Filter} via GraphQL",
|
||||
string.IsNullOrEmpty(searchName) ? "" : $" matching '{searchName}'");
|
||||
return new List<SpotifyPlaylist>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user