From 038c3a96147b0ebccf99300940a710e6c671cdb2 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Wed, 4 Feb 2026 16:12:41 -0500 Subject: [PATCH] Fix playlist count caching and make external tracks perfectly mimic Jellyfin responses - Fixed UpdateSpotifyPlaylistCounts to properly handle file cache without skipping items - Added Genres and GenreItems fields to all tracks (empty array if no genre) - Added complete MediaStreams with audio codec info for external tracks - Added missing MediaSource fields: IgnoreDts, IgnoreIndex, GenPtsInput, HasSegments - Ensured Artists array never contains null values - All external tracks now have proper genre arrays to match Jellyfin structure --- allstarr/Controllers/JellyfinController.cs | 88 ++++++++++--------- .../Jellyfin/JellyfinResponseBuilder.cs | 56 +++++++++--- 2 files changed, 90 insertions(+), 54 deletions(-) diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 35ddc91..6c0aee6 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -2780,55 +2780,57 @@ public class JellyfinController : ControllerBase // Use file cache count directly itemDict["ChildCount"] = fileItems.Count; modified = true; - updatedItems.Add(itemDict); - continue; } } - // Get local tracks count from Jellyfin - var localTracksCount = 0; - try + // Only fetch from Jellyfin if we didn't get count from file cache + if (!itemDict.ContainsKey("ChildCount") || (int)itemDict["ChildCount"]! == 0) { - var (localTracksResponse, _) = await _proxyService.GetJsonAsync( - $"Playlists/{playlistId}/Items", - null, - Request.Headers); - - if (localTracksResponse != null && - localTracksResponse.RootElement.TryGetProperty("Items", out var localItems)) + // Get local tracks count from Jellyfin + var localTracksCount = 0; + try { - localTracksCount = localItems.GetArrayLength(); - _logger.LogInformation("Found {Count} total items in Jellyfin playlist {Name}", - localTracksCount, playlistName); + var (localTracksResponse, _) = await _proxyService.GetJsonAsync( + $"Playlists/{playlistId}/Items", + null, + Request.Headers); + + if (localTracksResponse != null && + localTracksResponse.RootElement.TryGetProperty("Items", out var localItems)) + { + localTracksCount = localItems.GetArrayLength(); + _logger.LogInformation("Found {Count} total items in Jellyfin playlist {Name}", + localTracksCount, playlistName); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to get local tracks count for {Name}", playlistName); + } + + // Count external matched tracks (not local) + var externalMatchedCount = 0; + if (matchedTracks != null) + { + externalMatchedCount = matchedTracks.Count(t => t.MatchedSong != null && !t.MatchedSong.IsLocal); + } + + // Total available tracks = what's actually in Jellyfin (local + external matched) + // This is what clients should see as the track count + var totalAvailableCount = localTracksCount; + + if (totalAvailableCount > 0) + { + // Update ChildCount to show actual available tracks + itemDict["ChildCount"] = totalAvailableCount; + modified = true; + _logger.LogInformation("✓ Updated ChildCount for Spotify playlist {Name} to {Total} (actual tracks in Jellyfin)", + playlistName, totalAvailableCount); + } + else + { + _logger.LogWarning("No tracks found in Jellyfin for {Name}", playlistName); } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to get local tracks count for {Name}", playlistName); - } - - // Count external matched tracks (not local) - var externalMatchedCount = 0; - if (matchedTracks != null) - { - externalMatchedCount = matchedTracks.Count(t => t.MatchedSong != null && !t.MatchedSong.IsLocal); - } - - // Total available tracks = what's actually in Jellyfin (local + external matched) - // This is what clients should see as the track count - var totalAvailableCount = localTracksCount; - - if (totalAvailableCount > 0) - { - // Update ChildCount to show actual available tracks - itemDict["ChildCount"] = totalAvailableCount; - modified = true; - _logger.LogInformation("✓ Updated ChildCount for Spotify playlist {Name} to {Total} (actual tracks in Jellyfin)", - playlistName, totalAvailableCount); - } - else - { - _logger.LogWarning("No tracks found in Jellyfin for {Name}", playlistName); } } } diff --git a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs index 0824155..a88b352 100644 --- a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs +++ b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs @@ -249,7 +249,7 @@ public class JellyfinResponseBuilder ["Album"] = song.Album, ["AlbumId"] = song.AlbumId ?? song.Id, ["AlbumArtist"] = song.AlbumArtist ?? song.Artist, - ["Artists"] = song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist }, + ["Artists"] = song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist ?? "" }, ["ArtistItems"] = song.Artists.Count > 0 ? song.Artists.Select((name, index) => new Dictionary { @@ -263,7 +263,7 @@ public class JellyfinResponseBuilder new Dictionary { ["Id"] = song.ArtistId ?? song.Id, - ["Name"] = song.Artist + ["Name"] = song.Artist ?? "" } }, ["IndexNumber"] = song.Track, @@ -288,7 +288,21 @@ public class JellyfinResponseBuilder ["Key"] = $"Audio-{song.Id}" }, ["CanDownload"] = true, - ["SupportsSync"] = true + ["SupportsSync"] = true, + // Always include Genres array - use song genre if available, otherwise use a default + ["Genres"] = !string.IsNullOrEmpty(song.Genre) + ? new[] { song.Genre } + : new string[0], // Empty array instead of null + ["GenreItems"] = !string.IsNullOrEmpty(song.Genre) + ? new[] + { + new Dictionary + { + ["Name"] = song.Genre, + ["Id"] = $"genre-{song.Genre?.ToLowerInvariant()}" + } + } + : new Dictionary[0] }; // Add provider IDs for external content @@ -327,15 +341,40 @@ public class JellyfinResponseBuilder ["RequiresLooping"] = false, ["SupportsProbing"] = true, ["ReadAtNativeFramerate"] = false, - ["MediaStreams"] = new List(), // Empty array instead of null + ["IgnoreDts"] = false, + ["IgnoreIndex"] = false, + ["GenPtsInput"] = false, + ["MediaStreams"] = new[] + { + new Dictionary + { + ["Codec"] = "flac", + ["TimeBase"] = "1/44100", + ["DisplayTitle"] = "FLAC - Stereo - Default", + ["IsInterlaced"] = false, + ["ChannelLayout"] = "stereo", + ["BitRate"] = 1337000, + ["Channels"] = 2, + ["SampleRate"] = 44100, + ["IsDefault"] = true, + ["IsForced"] = false, + ["Type"] = "Audio", + ["Index"] = 0, + ["IsExternal"] = false, + ["IsTextSubtitleStream"] = false, + ["SupportsExternalStream"] = false, + ["Level"] = 0 + } + }, ["MediaAttachments"] = new List(), // Empty array instead of null - ["Formats"] = new List(), // Empty array instead of null + ["Formats"] = new List(), // Empty array instead of null ["RequiredHttpHeaders"] = new Dictionary(), // Empty dict instead of null ["RunTimeTicks"] = (song.Duration ?? 180) * 10000000L, // Duration in ticks (100ns units) ["Name"] = song.Title, ["AnalyzeDurationMs"] = 0, ["DefaultAudioStreamIndex"] = 0, - ["DefaultSubtitleStreamIndex"] = -1 + ["DefaultSubtitleStreamIndex"] = -1, + ["HasSegments"] = false } }; } @@ -345,11 +384,6 @@ public class JellyfinResponseBuilder item["MediaSources"] = song.JellyfinMetadata["MediaSources"]; } - if (!string.IsNullOrEmpty(song.Genre)) - { - item["Genres"] = new[] { song.Genre }; - } - return item; }