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
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);
}
}
}

View File

@@ -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<string, object?>
{
@@ -263,7 +263,7 @@ public class JellyfinResponseBuilder
new Dictionary<string, object?>
{
["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<string, object?>
{
["Name"] = song.Genre,
["Id"] = $"genre-{song.Genre?.ToLowerInvariant()}"
}
}
: new Dictionary<string, object?>[0]
};
// Add provider IDs for external content
@@ -327,15 +341,40 @@ public class JellyfinResponseBuilder
["RequiresLooping"] = false,
["SupportsProbing"] = true,
["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
["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
["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;
}