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
This commit is contained in:
2026-02-04 16:12:41 -05:00
parent 6e966f9e0d
commit 038c3a9614
2 changed files with 90 additions and 54 deletions

View File

@@ -2780,55 +2780,57 @@ public class JellyfinController : ControllerBase
// Use file cache count directly // Use file cache count directly
itemDict["ChildCount"] = fileItems.Count; itemDict["ChildCount"] = fileItems.Count;
modified = true; modified = true;
updatedItems.Add(itemDict);
continue;
} }
} }
// Get local tracks count from Jellyfin // Only fetch from Jellyfin if we didn't get count from file cache
var localTracksCount = 0; if (!itemDict.ContainsKey("ChildCount") || (int)itemDict["ChildCount"]! == 0)
try
{ {
var (localTracksResponse, _) = await _proxyService.GetJsonAsync( // Get local tracks count from Jellyfin
$"Playlists/{playlistId}/Items", var localTracksCount = 0;
null, try
Request.Headers);
if (localTracksResponse != null &&
localTracksResponse.RootElement.TryGetProperty("Items", out var localItems))
{ {
localTracksCount = localItems.GetArrayLength(); var (localTracksResponse, _) = await _proxyService.GetJsonAsync(
_logger.LogInformation("Found {Count} total items in Jellyfin playlist {Name}", $"Playlists/{playlistId}/Items",
localTracksCount, playlistName); 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);
} }
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get local tracks count for {Name}", playlistName);
}
// Count external matched tracks (not local) // Count external matched tracks (not local)
var externalMatchedCount = 0; var externalMatchedCount = 0;
if (matchedTracks != null) if (matchedTracks != null)
{ {
externalMatchedCount = matchedTracks.Count(t => t.MatchedSong != null && !t.MatchedSong.IsLocal); externalMatchedCount = matchedTracks.Count(t => t.MatchedSong != null && !t.MatchedSong.IsLocal);
} }
// Total available tracks = what's actually in Jellyfin (local + external matched) // Total available tracks = what's actually in Jellyfin (local + external matched)
// This is what clients should see as the track count // This is what clients should see as the track count
var totalAvailableCount = localTracksCount; var totalAvailableCount = localTracksCount;
if (totalAvailableCount > 0) if (totalAvailableCount > 0)
{ {
// Update ChildCount to show actual available tracks // Update ChildCount to show actual available tracks
itemDict["ChildCount"] = totalAvailableCount; itemDict["ChildCount"] = totalAvailableCount;
modified = true; modified = true;
_logger.LogInformation("✓ Updated ChildCount for Spotify playlist {Name} to {Total} (actual tracks in Jellyfin)", _logger.LogInformation("✓ Updated ChildCount for Spotify playlist {Name} to {Total} (actual tracks in Jellyfin)",
playlistName, totalAvailableCount); playlistName, totalAvailableCount);
} }
else else
{ {
_logger.LogWarning("No tracks found in Jellyfin for {Name}", playlistName); _logger.LogWarning("No tracks found in Jellyfin for {Name}", playlistName);
}
} }
} }
} }

View File

@@ -249,7 +249,7 @@ public class JellyfinResponseBuilder
["Album"] = song.Album, ["Album"] = song.Album,
["AlbumId"] = song.AlbumId ?? song.Id, ["AlbumId"] = song.AlbumId ?? song.Id,
["AlbumArtist"] = song.AlbumArtist ?? song.Artist, ["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 ["ArtistItems"] = song.Artists.Count > 0
? song.Artists.Select((name, index) => new Dictionary<string, object?> ? song.Artists.Select((name, index) => new Dictionary<string, object?>
{ {
@@ -263,7 +263,7 @@ public class JellyfinResponseBuilder
new Dictionary<string, object?> new Dictionary<string, object?>
{ {
["Id"] = song.ArtistId ?? song.Id, ["Id"] = song.ArtistId ?? song.Id,
["Name"] = song.Artist ["Name"] = song.Artist ?? ""
} }
}, },
["IndexNumber"] = song.Track, ["IndexNumber"] = song.Track,
@@ -288,7 +288,21 @@ public class JellyfinResponseBuilder
["Key"] = $"Audio-{song.Id}" ["Key"] = $"Audio-{song.Id}"
}, },
["CanDownload"] = true, ["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<string, object?>
{
["Name"] = song.Genre,
["Id"] = $"genre-{song.Genre?.ToLowerInvariant()}"
}
}
: new Dictionary<string, object?>[0]
}; };
// Add provider IDs for external content // Add provider IDs for external content
@@ -327,15 +341,40 @@ public class JellyfinResponseBuilder
["RequiresLooping"] = false, ["RequiresLooping"] = false,
["SupportsProbing"] = true, ["SupportsProbing"] = true,
["ReadAtNativeFramerate"] = false, ["ReadAtNativeFramerate"] = false,
["MediaStreams"] = new List<object>(), // Empty array instead of null ["IgnoreDts"] = false,
["IgnoreIndex"] = false,
["GenPtsInput"] = false,
["MediaStreams"] = new[]
{
new Dictionary<string, object?>
{
["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<object>(), // Empty array instead of null ["MediaAttachments"] = new List<object>(), // Empty array instead of null
["Formats"] = new List<object>(), // Empty array instead of null ["Formats"] = new List<string>(), // Empty array instead of null
["RequiredHttpHeaders"] = new Dictionary<string, string>(), // Empty dict instead of null ["RequiredHttpHeaders"] = new Dictionary<string, string>(), // Empty dict instead of null
["RunTimeTicks"] = (song.Duration ?? 180) * 10000000L, // Duration in ticks (100ns units) ["RunTimeTicks"] = (song.Duration ?? 180) * 10000000L, // Duration in ticks (100ns units)
["Name"] = song.Title, ["Name"] = song.Title,
["AnalyzeDurationMs"] = 0, ["AnalyzeDurationMs"] = 0,
["DefaultAudioStreamIndex"] = 0, ["DefaultAudioStreamIndex"] = 0,
["DefaultSubtitleStreamIndex"] = -1 ["DefaultSubtitleStreamIndex"] = -1,
["HasSegments"] = false
} }
}; };
} }
@@ -345,11 +384,6 @@ public class JellyfinResponseBuilder
item["MediaSources"] = song.JellyfinMetadata["MediaSources"]; item["MediaSources"] = song.JellyfinMetadata["MediaSources"];
} }
if (!string.IsNullOrEmpty(song.Genre))
{
item["Genres"] = new[] { song.Genre };
}
return item; return item;
} }