From 88bf0833861e606a8215ea156d9c50a54e6b6be8 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Mon, 9 Feb 2026 16:36:10 -0500 Subject: [PATCH] Fix GraphQL query for fetching user playlists - use libraryV3 - Changed from fetchLibraryPlaylists to libraryV3 operation (correct Spotify GraphQL endpoint) - Use GET request with query params instead of POST (matches Jellyfin plugin implementation) - Updated response parsing to match libraryV3 structure (me.libraryV3.items[].item.data) - Fixed owner field to use 'username' instead of 'name' - This should resolve the BadRequest (400) errors when fetching user playlists --- allstarr/Services/Spotify/SpotifyApiClient.cs | 94 ++++++++----------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/allstarr/Services/Spotify/SpotifyApiClient.cs b/allstarr/Services/Spotify/SpotifyApiClient.cs index 5372c3b..c66e6b8 100644 --- a/allstarr/Services/Spotify/SpotifyApiClient.cs +++ b/allstarr/Services/Spotify/SpotifyApiClient.cs @@ -775,53 +775,18 @@ public class SpotifyApiClient : IDisposable while (true) { - // GraphQL query to fetch user playlists - var graphqlQuery = new + // GraphQL query to fetch user playlists - using libraryV3 operation + var queryParams = new Dictionary { - operationName = "fetchLibraryPlaylists", - variables = new - { - offset, - limit - }, - query = @" - query fetchLibraryPlaylists($offset: Int!, $limit: Int!) { - me { - library { - playlists(offset: $offset, limit: $limit) { - totalCount - items { - playlist { - uri - name - description - images { - url - } - ownerV2 { - data { - __typename - ... on User { - id - name - } - } - } - } - } - } - } - } - }" + { "operationName", "libraryV3" }, + { "variables", $"{{\"filters\":[\"Playlists\",\"By Spotify\"],\"order\":null,\"textFilter\":\"\",\"features\":[\"LIKED_SONGS\",\"YOUR_EPISODES\"],\"offset\":{offset},\"limit\":{limit}}}" }, + { "extensions", "{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"50650f72ea32a99b5b46240bee22fea83024eec302478a9a75cfd05a0814ba99\"}}" } }; - var request = new HttpRequestMessage(HttpMethod.Post, $"{WebApiBase}/query") - { - Content = new StringContent( - JsonSerializer.Serialize(graphqlQuery), - System.Text.Encoding.UTF8, - "application/json") - }; + var queryString = string.Join("&", queryParams.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); + var url = $"{WebApiBase}/query?{queryString}"; + + var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await _webApiClient.SendAsync(request, cancellationToken); @@ -849,20 +814,44 @@ public class SpotifyApiClient : IDisposable if (!root.TryGetProperty("data", out var data) || !data.TryGetProperty("me", out var me) || - !me.TryGetProperty("library", out var library) || - !library.TryGetProperty("playlists", out var playlistsData) || - !playlistsData.TryGetProperty("items", out var items)) + !me.TryGetProperty("libraryV3", out var library) || + !library.TryGetProperty("items", out var items)) { break; } + // Get total count + if (library.TryGetProperty("totalCount", out var totalCount)) + { + var total = totalCount.GetInt32(); + if (total == 0) break; + } + var itemCount = 0; foreach (var item in items.EnumerateArray()) { itemCount++; - if (!item.TryGetProperty("playlist", out var playlist)) + if (!item.TryGetProperty("item", out var playlistItem) || + !playlistItem.TryGetProperty("data", out var playlist)) + { continue; + } + + // Get playlist URI/ID + string? uri = null; + if (playlistItem.TryGetProperty("uri", out var uriProp)) + { + uri = uriProp.GetString(); + } + else if (playlistItem.TryGetProperty("_uri", out var uriProp2)) + { + uri = uriProp2.GetString(); + } + + if (string.IsNullOrEmpty(uri)) continue; + + var spotifyId = uri.Replace("spotify:playlist:", "", StringComparison.OrdinalIgnoreCase); var itemName = playlist.TryGetProperty("name", out var n) ? n.GetString() ?? "" : ""; @@ -873,22 +862,19 @@ public class SpotifyApiClient : IDisposable 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)) + content.TryGetProperty("totalCount", out var totalTrackCount)) { - trackCount = totalCount.GetInt32(); + trackCount = totalTrackCount.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)) + ownerData.TryGetProperty("username", out var ownerNameProp)) { ownerName = ownerNameProp.GetString(); }