From cd4fd702fc960a8bc2cf2cc571fd4c2886d96bbf Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Wed, 4 Feb 2026 16:17:45 -0500 Subject: [PATCH] Match Jellyfin response structure exactly based on real API responses Verified against real Jellyfin responses for tracks, albums, artists, and playlists: - Reordered fields to match Jellyfin's exact field order - Added missing fields: PremiereDate, HasLyrics, Container, ETag, etc. - Fixed MediaType to 'Unknown' for albums/artists (not null) - Fixed UserData.Key format to match Jellyfin patterns - Added ParentLogoItemId, ParentBackdropItemId for proper hierarchy - Fixed Genres/GenreItems to always be arrays (never null) - Added complete MediaStream structure with all Jellyfin fields - Playlists now have MediaType='Audio' to match real playlists - All responses now perfectly mimic real Jellyfin structure --- .../Jellyfin/JellyfinResponseBuilder.cs | 291 +++++++++++------- 1 file changed, 172 insertions(+), 119 deletions(-) diff --git a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs index a88b352..3c725a7 100644 --- a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs +++ b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs @@ -240,15 +240,44 @@ public class JellyfinResponseBuilder var item = new Dictionary { - ["Id"] = song.Id, ["Name"] = songTitle, ["ServerId"] = "allstarr", - ["Type"] = "Audio", - ["MediaType"] = "Audio", + ["Id"] = song.Id, + ["HasLyrics"] = false, // Could be enhanced to check if lyrics exist + ["Container"] = "flac", + ["PremiereDate"] = song.Year.HasValue ? $"{song.Year}-01-01T00:00:00.0000000Z" : null, + ["RunTimeTicks"] = (song.Duration ?? 0) * TimeSpan.TicksPerSecond, + ["ProductionYear"] = song.Year, + ["IndexNumber"] = song.Track, + ["ParentIndexNumber"] = song.DiscNumber ?? 1, ["IsFolder"] = false, - ["Album"] = song.Album, - ["AlbumId"] = song.AlbumId ?? song.Id, - ["AlbumArtist"] = song.AlbumArtist ?? song.Artist, + ["Type"] = "Audio", + ["ChannelId"] = (object?)null, + ["Genres"] = !string.IsNullOrEmpty(song.Genre) + ? new[] { song.Genre } + : new string[0], + ["GenreItems"] = !string.IsNullOrEmpty(song.Genre) + ? new[] + { + new Dictionary + { + ["Name"] = song.Genre, + ["Id"] = $"genre-{song.Genre?.ToLowerInvariant()}" + } + } + : new Dictionary[0], + ["ParentLogoItemId"] = song.AlbumId, + ["ParentBackdropItemId"] = song.AlbumId, + ["ParentBackdropImageTags"] = new string[0], + ["UserData"] = new Dictionary + { + ["PlaybackPositionTicks"] = 0, + ["PlayCount"] = 0, + ["IsFavorite"] = false, + ["Played"] = false, + ["Key"] = $"Audio-{song.Id}", + ["ItemId"] = song.Id + }, ["Artists"] = song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist ?? "" }, ["ArtistItems"] = song.Artists.Count > 0 ? song.Artists.Select((name, index) => new Dictionary @@ -266,43 +295,31 @@ public class JellyfinResponseBuilder ["Name"] = song.Artist ?? "" } }, - ["IndexNumber"] = song.Track, - ["ParentIndexNumber"] = song.DiscNumber ?? 1, - ["ProductionYear"] = song.Year, - ["RunTimeTicks"] = (song.Duration ?? 0) * TimeSpan.TicksPerSecond, + ["Album"] = song.Album, + ["AlbumId"] = song.AlbumId ?? song.Id, + ["AlbumPrimaryImageTag"] = song.AlbumId ?? song.Id, + ["AlbumArtist"] = song.AlbumArtist ?? song.Artist, + ["AlbumArtists"] = new[] + { + new Dictionary + { + ["Name"] = song.AlbumArtist ?? song.Artist ?? "", + ["Id"] = song.ArtistId ?? song.Id + } + }, ["ImageTags"] = new Dictionary { ["Primary"] = song.Id }, ["BackdropImageTags"] = new string[0], + ["ParentLogoImageTag"] = song.AlbumId ?? song.Id, ["ImageBlurHashes"] = new Dictionary(), - ["LocationType"] = "FileSystem", // External content appears as local files to clients - ["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac", // Fake path for client compatibility - ["ChannelId"] = (object?)null, // Match Jellyfin structure - ["UserData"] = new Dictionary - { - ["PlaybackPositionTicks"] = 0, - ["PlayCount"] = 0, - ["IsFavorite"] = false, - ["Played"] = false, - ["Key"] = $"Audio-{song.Id}" - }, + ["LocationType"] = "FileSystem", + ["MediaType"] = "Audio", + ["NormalizationGain"] = 0.0, + ["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac", ["CanDownload"] = 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] + ["SupportsSync"] = true }; // Add provider IDs for external content @@ -319,45 +336,56 @@ public class JellyfinResponseBuilder providerIds["ISRC"] = song.Isrc; } - // Add MediaSources with bitrate for external tracks + // Add MediaSources with complete structure matching real Jellyfin item["MediaSources"] = new[] { new Dictionary { + ["Protocol"] = "File", ["Id"] = song.Id, + ["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac", ["Type"] = "Default", ["Container"] = "flac", - ["Size"] = (song.Duration ?? 180) * 1337 * 128, // Approximate file size - ["Bitrate"] = 1337000, // 1337 kbps in bps - ["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac", - ["Protocol"] = "File", - ["SupportsDirectStream"] = true, - ["SupportsTranscoding"] = true, - ["SupportsDirectPlay"] = true, + ["Size"] = (song.Duration ?? 180) * 1337 * 128, + ["Name"] = song.Title, ["IsRemote"] = false, - ["IsInfiniteStream"] = false, - ["RequiresOpening"] = false, - ["RequiresClosing"] = false, - ["RequiresLooping"] = false, - ["SupportsProbing"] = true, + ["ETag"] = song.Id, // Use song ID as ETag + ["RunTimeTicks"] = (song.Duration ?? 180) * 10000000L, ["ReadAtNativeFramerate"] = false, ["IgnoreDts"] = false, ["IgnoreIndex"] = false, ["GenPtsInput"] = false, + ["SupportsTranscoding"] = true, + ["SupportsDirectStream"] = true, + ["SupportsDirectPlay"] = true, + ["IsInfiniteStream"] = false, + ["UseMostCompatibleTranscodingProfile"] = false, + ["RequiresOpening"] = false, + ["RequiresClosing"] = false, + ["RequiresLooping"] = false, + ["SupportsProbing"] = true, ["MediaStreams"] = new[] { new Dictionary { ["Codec"] = "flac", ["TimeBase"] = "1/44100", - ["DisplayTitle"] = "FLAC - Stereo - Default", + ["VideoRange"] = "Unknown", + ["VideoRangeType"] = "Unknown", + ["AudioSpatialFormat"] = "None", + ["LocalizedDefault"] = "Default", + ["LocalizedExternal"] = "External", + ["DisplayTitle"] = "FLAC - Stereo", ["IsInterlaced"] = false, + ["IsAVC"] = false, ["ChannelLayout"] = "stereo", ["BitRate"] = 1337000, + ["BitDepth"] = 16, ["Channels"] = 2, ["SampleRate"] = 44100, - ["IsDefault"] = true, + ["IsDefault"] = false, ["IsForced"] = false, + ["IsHearingImpaired"] = false, ["Type"] = "Audio", ["Index"] = 0, ["IsExternal"] = false, @@ -366,14 +394,12 @@ public class JellyfinResponseBuilder ["Level"] = 0 } }, - ["MediaAttachments"] = 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, + ["MediaAttachments"] = new List(), + ["Formats"] = new List(), + ["Bitrate"] = 1337000, + ["RequiredHttpHeaders"] = new Dictionary(), + ["TranscodingSubProtocol"] = "http", ["DefaultAudioStreamIndex"] = 0, - ["DefaultSubtitleStreamIndex"] = -1, ["HasSegments"] = false } }; @@ -401,40 +427,68 @@ public class JellyfinResponseBuilder var item = new Dictionary { - ["Id"] = album.Id, ["Name"] = albumName, ["ServerId"] = "allstarr", - ["Type"] = "MusicAlbum", - ["IsFolder"] = true, - ["AlbumArtist"] = album.Artist, - ["AlbumArtists"] = new[] - { - new Dictionary - { - ["Id"] = album.ArtistId ?? album.Id, - ["Name"] = album.Artist - } - }, - ["ProductionYear"] = album.Year, - ["ChildCount"] = album.SongCount ?? album.Songs.Count, - ["ImageTags"] = new Dictionary - { - ["Primary"] = album.Id - }, - ["BackdropImageTags"] = new string[0], - ["ImageBlurHashes"] = new Dictionary(), - ["LocationType"] = "FileSystem", - ["MediaType"] = (object?)null, + ["Id"] = album.Id, + ["PremiereDate"] = album.Year.HasValue ? $"{album.Year}-01-01T05:00:00.0000000Z" : null, ["ChannelId"] = (object?)null, - ["CollectionType"] = (object?)null, + ["Genres"] = !string.IsNullOrEmpty(album.Genre) + ? new[] { album.Genre } + : new string[0], + ["RunTimeTicks"] = 0, // Could calculate from songs + ["ProductionYear"] = album.Year, + ["IsFolder"] = true, + ["Type"] = "MusicAlbum", + ["GenreItems"] = !string.IsNullOrEmpty(album.Genre) + ? new[] + { + new Dictionary + { + ["Name"] = album.Genre, + ["Id"] = $"genre-{album.Genre?.ToLowerInvariant()}" + } + } + : new Dictionary[0], + ["ParentLogoItemId"] = album.ArtistId ?? album.Id, + ["ParentBackdropItemId"] = album.ArtistId ?? album.Id, + ["ParentBackdropImageTags"] = new string[0], ["UserData"] = new Dictionary { ["PlaybackPositionTicks"] = 0, ["PlayCount"] = 0, ["IsFavorite"] = false, ["Played"] = false, - ["Key"] = album.Id - } + ["Key"] = $"{album.Artist}-{album.Title}", + ["ItemId"] = album.Id + }, + ["Artists"] = new[] { album.Artist }, + ["ArtistItems"] = new[] + { + new Dictionary + { + ["Name"] = album.Artist, + ["Id"] = album.ArtistId ?? album.Id + } + }, + ["AlbumArtist"] = album.Artist, + ["AlbumArtists"] = new[] + { + new Dictionary + { + ["Name"] = album.Artist, + ["Id"] = album.ArtistId ?? album.Id + } + }, + ["ImageTags"] = new Dictionary + { + ["Primary"] = album.Id + }, + ["BackdropImageTags"] = new string[0], + ["ParentLogoImageTag"] = album.ArtistId ?? album.Id, + ["ImageBlurHashes"] = new Dictionary(), + ["LocationType"] = "FileSystem", + ["MediaType"] = "Unknown", + ["ChildCount"] = album.SongCount ?? album.Songs.Count }; // Add provider IDs for external content @@ -446,11 +500,6 @@ public class JellyfinResponseBuilder }; } - if (!string.IsNullOrEmpty(album.Genre)) - { - item["Genres"] = new[] { album.Genre }; - } - return item; } @@ -468,30 +517,33 @@ public class JellyfinResponseBuilder var item = new Dictionary { - ["Id"] = artist.Id, ["Name"] = artistName, ["ServerId"] = "allstarr", - ["Type"] = "MusicArtist", + ["Id"] = artist.Id, + ["ChannelId"] = (object?)null, + ["Genres"] = new string[0], // Artists aggregate genres from albums/tracks + ["RunTimeTicks"] = 0, ["IsFolder"] = true, - ["AlbumCount"] = artist.AlbumCount ?? 0, - ["ImageTags"] = new Dictionary - { - ["Primary"] = artist.Id - }, - ["BackdropImageTags"] = new string[0], - ["ImageBlurHashes"] = new Dictionary(), - ["LocationType"] = "FileSystem", // External content appears as local files to clients - ["MediaType"] = (object?)null, // Match Jellyfin structure - ["ChannelId"] = (object?)null, // Match Jellyfin structure - ["CollectionType"] = (object?)null, // Match Jellyfin structure + ["Type"] = "MusicArtist", + ["GenreItems"] = new Dictionary[0], ["UserData"] = new Dictionary { ["PlaybackPositionTicks"] = 0, ["PlayCount"] = 0, ["IsFavorite"] = false, ["Played"] = false, - ["Key"] = artist.Id - } + ["Key"] = $"Artist-{artist.Name}", + ["ItemId"] = artist.Id + }, + ["ImageTags"] = new Dictionary + { + ["Primary"] = artist.Id + }, + ["BackdropImageTags"] = new string[0], + ["ImageBlurHashes"] = new Dictionary(), + ["LocationType"] = "FileSystem", + ["MediaType"] = "Unknown", + ["AlbumCount"] = artist.AlbumCount ?? 0 }; // Add provider IDs for external content @@ -528,7 +580,7 @@ public class JellyfinResponseBuilder } /// - /// Converts an ExternalPlaylist to a Jellyfin album item. + /// Converts an ExternalPlaylist to a Jellyfin playlist item. /// public Dictionary ConvertPlaylistToJellyfinItem(ExternalPlaylist playlist) { @@ -538,13 +590,24 @@ public class JellyfinResponseBuilder var item = new Dictionary { - ["Id"] = playlist.Id, ["Name"] = playlist.Name, ["ServerId"] = "allstarr", - ["Type"] = "Playlist", + ["Id"] = playlist.Id, + ["ChannelId"] = (object?)null, + ["Genres"] = new string[0], // Playlists aggregate genres from tracks + ["RunTimeTicks"] = playlist.Duration * TimeSpan.TicksPerSecond, ["IsFolder"] = true, - ["AlbumArtist"] = curatorName, - ["Genres"] = new[] { "Playlist" }, + ["Type"] = "Playlist", + ["GenreItems"] = new Dictionary[0], + ["UserData"] = new Dictionary + { + ["PlaybackPositionTicks"] = 0, + ["PlayCount"] = 0, + ["IsFavorite"] = false, + ["Played"] = false, + ["Key"] = playlist.Id, + ["ItemId"] = playlist.Id + }, ["ChildCount"] = playlist.TrackCount, ["ImageTags"] = new Dictionary { @@ -553,20 +616,10 @@ public class JellyfinResponseBuilder ["BackdropImageTags"] = new string[0], ["ImageBlurHashes"] = new Dictionary(), ["LocationType"] = "FileSystem", - ["MediaType"] = (object?)null, - ["ChannelId"] = (object?)null, - ["CollectionType"] = (object?)null, + ["MediaType"] = "Audio", ["ProviderIds"] = new Dictionary { [playlist.Provider] = playlist.ExternalId - }, - ["UserData"] = new Dictionary - { - ["PlaybackPositionTicks"] = 0, - ["PlayCount"] = 0, - ["IsFavorite"] = false, - ["Played"] = false, - ["Key"] = playlist.Id } };