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
This commit is contained in:
2026-02-09 16:36:10 -05:00
parent 670544a9d6
commit 88bf083386

View File

@@ -775,53 +775,18 @@ public class SpotifyApiClient : IDisposable
while (true) while (true)
{ {
// GraphQL query to fetch user playlists // GraphQL query to fetch user playlists - using libraryV3 operation
var graphqlQuery = new var queryParams = new Dictionary<string, string>
{ {
operationName = "fetchLibraryPlaylists", { "operationName", "libraryV3" },
variables = new { "variables", $"{{\"filters\":[\"Playlists\",\"By Spotify\"],\"order\":null,\"textFilter\":\"\",\"features\":[\"LIKED_SONGS\",\"YOUR_EPISODES\"],\"offset\":{offset},\"limit\":{limit}}}" },
{ { "extensions", "{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"50650f72ea32a99b5b46240bee22fea83024eec302478a9a75cfd05a0814ba99\"}}" }
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
}
}
}
}
}
}
}
}
}"
}; };
var request = new HttpRequestMessage(HttpMethod.Post, $"{WebApiBase}/query") var queryString = string.Join("&", queryParams.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"));
{ var url = $"{WebApiBase}/query?{queryString}";
Content = new StringContent(
JsonSerializer.Serialize(graphqlQuery), var request = new HttpRequestMessage(HttpMethod.Get, url);
System.Text.Encoding.UTF8,
"application/json")
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _webApiClient.SendAsync(request, cancellationToken); var response = await _webApiClient.SendAsync(request, cancellationToken);
@@ -849,20 +814,44 @@ public class SpotifyApiClient : IDisposable
if (!root.TryGetProperty("data", out var data) || if (!root.TryGetProperty("data", out var data) ||
!data.TryGetProperty("me", out var me) || !data.TryGetProperty("me", out var me) ||
!me.TryGetProperty("library", out var library) || !me.TryGetProperty("libraryV3", out var library) ||
!library.TryGetProperty("playlists", out var playlistsData) || !library.TryGetProperty("items", out var items))
!playlistsData.TryGetProperty("items", out var items))
{ {
break; break;
} }
// Get total count
if (library.TryGetProperty("totalCount", out var totalCount))
{
var total = totalCount.GetInt32();
if (total == 0) break;
}
var itemCount = 0; var itemCount = 0;
foreach (var item in items.EnumerateArray()) foreach (var item in items.EnumerateArray())
{ {
itemCount++; itemCount++;
if (!item.TryGetProperty("playlist", out var playlist)) if (!item.TryGetProperty("item", out var playlistItem) ||
!playlistItem.TryGetProperty("data", out var playlist))
{
continue; 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() ?? "" : ""; var itemName = playlist.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
@@ -873,22 +862,19 @@ public class SpotifyApiClient : IDisposable
continue; continue;
} }
var uri = playlist.TryGetProperty("uri", out var u) ? u.GetString() ?? "" : "";
var spotifyId = uri.Replace("spotify:playlist:", "", StringComparison.OrdinalIgnoreCase);
// Get track count if available // Get track count if available
var trackCount = 0; var trackCount = 0;
if (playlist.TryGetProperty("content", out var content) && 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 // Get owner name
string? ownerName = null; string? ownerName = null;
if (playlist.TryGetProperty("ownerV2", out var ownerV2) && if (playlist.TryGetProperty("ownerV2", out var ownerV2) &&
ownerV2.TryGetProperty("data", out var ownerData) && ownerV2.TryGetProperty("data", out var ownerData) &&
ownerData.TryGetProperty("name", out var ownerNameProp)) ownerData.TryGetProperty("username", out var ownerNameProp))
{ {
ownerName = ownerNameProp.GetString(); ownerName = ownerNameProp.GetString();
} }