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
This commit is contained in:
2026-02-04 16:17:45 -05:00
parent 038c3a9614
commit cd4fd702fc

View File

@@ -240,15 +240,44 @@ public class JellyfinResponseBuilder
var item = new Dictionary<string, object?> var item = new Dictionary<string, object?>
{ {
["Id"] = song.Id,
["Name"] = songTitle, ["Name"] = songTitle,
["ServerId"] = "allstarr", ["ServerId"] = "allstarr",
["Type"] = "Audio", ["Id"] = song.Id,
["MediaType"] = "Audio", ["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, ["IsFolder"] = false,
["Album"] = song.Album, ["Type"] = "Audio",
["AlbumId"] = song.AlbumId ?? song.Id, ["ChannelId"] = (object?)null,
["AlbumArtist"] = song.AlbumArtist ?? song.Artist, ["Genres"] = !string.IsNullOrEmpty(song.Genre)
? new[] { song.Genre }
: new string[0],
["GenreItems"] = !string.IsNullOrEmpty(song.Genre)
? new[]
{
new Dictionary<string, object?>
{
["Name"] = song.Genre,
["Id"] = $"genre-{song.Genre?.ToLowerInvariant()}"
}
}
: new Dictionary<string, object?>[0],
["ParentLogoItemId"] = song.AlbumId,
["ParentBackdropItemId"] = song.AlbumId,
["ParentBackdropImageTags"] = new string[0],
["UserData"] = new Dictionary<string, object>
{
["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 ?? "" }, ["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?>
@@ -266,43 +295,31 @@ public class JellyfinResponseBuilder
["Name"] = song.Artist ?? "" ["Name"] = song.Artist ?? ""
} }
}, },
["IndexNumber"] = song.Track, ["Album"] = song.Album,
["ParentIndexNumber"] = song.DiscNumber ?? 1, ["AlbumId"] = song.AlbumId ?? song.Id,
["ProductionYear"] = song.Year, ["AlbumPrimaryImageTag"] = song.AlbumId ?? song.Id,
["RunTimeTicks"] = (song.Duration ?? 0) * TimeSpan.TicksPerSecond, ["AlbumArtist"] = song.AlbumArtist ?? song.Artist,
["AlbumArtists"] = new[]
{
new Dictionary<string, object?>
{
["Name"] = song.AlbumArtist ?? song.Artist ?? "",
["Id"] = song.ArtistId ?? song.Id
}
},
["ImageTags"] = new Dictionary<string, string> ["ImageTags"] = new Dictionary<string, string>
{ {
["Primary"] = song.Id ["Primary"] = song.Id
}, },
["BackdropImageTags"] = new string[0], ["BackdropImageTags"] = new string[0],
["ParentLogoImageTag"] = song.AlbumId ?? song.Id,
["ImageBlurHashes"] = new Dictionary<string, object>(), ["ImageBlurHashes"] = new Dictionary<string, object>(),
["LocationType"] = "FileSystem", // External content appears as local files to clients ["LocationType"] = "FileSystem",
["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac", // Fake path for client compatibility ["MediaType"] = "Audio",
["ChannelId"] = (object?)null, // Match Jellyfin structure ["NormalizationGain"] = 0.0,
["UserData"] = new Dictionary<string, object> ["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac",
{
["PlaybackPositionTicks"] = 0,
["PlayCount"] = 0,
["IsFavorite"] = false,
["Played"] = false,
["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
@@ -319,45 +336,56 @@ public class JellyfinResponseBuilder
providerIds["ISRC"] = song.Isrc; providerIds["ISRC"] = song.Isrc;
} }
// Add MediaSources with bitrate for external tracks // Add MediaSources with complete structure matching real Jellyfin
item["MediaSources"] = new[] item["MediaSources"] = new[]
{ {
new Dictionary<string, object?> new Dictionary<string, object?>
{ {
["Protocol"] = "File",
["Id"] = song.Id, ["Id"] = song.Id,
["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac",
["Type"] = "Default", ["Type"] = "Default",
["Container"] = "flac", ["Container"] = "flac",
["Size"] = (song.Duration ?? 180) * 1337 * 128, // Approximate file size ["Size"] = (song.Duration ?? 180) * 1337 * 128,
["Bitrate"] = 1337000, // 1337 kbps in bps ["Name"] = song.Title,
["Path"] = $"/music/{song.Artist}/{song.Album}/{song.Title}.flac",
["Protocol"] = "File",
["SupportsDirectStream"] = true,
["SupportsTranscoding"] = true,
["SupportsDirectPlay"] = true,
["IsRemote"] = false, ["IsRemote"] = false,
["IsInfiniteStream"] = false, ["ETag"] = song.Id, // Use song ID as ETag
["RequiresOpening"] = false, ["RunTimeTicks"] = (song.Duration ?? 180) * 10000000L,
["RequiresClosing"] = false,
["RequiresLooping"] = false,
["SupportsProbing"] = true,
["ReadAtNativeFramerate"] = false, ["ReadAtNativeFramerate"] = false,
["IgnoreDts"] = false, ["IgnoreDts"] = false,
["IgnoreIndex"] = false, ["IgnoreIndex"] = false,
["GenPtsInput"] = false, ["GenPtsInput"] = false,
["SupportsTranscoding"] = true,
["SupportsDirectStream"] = true,
["SupportsDirectPlay"] = true,
["IsInfiniteStream"] = false,
["UseMostCompatibleTranscodingProfile"] = false,
["RequiresOpening"] = false,
["RequiresClosing"] = false,
["RequiresLooping"] = false,
["SupportsProbing"] = true,
["MediaStreams"] = new[] ["MediaStreams"] = new[]
{ {
new Dictionary<string, object?> new Dictionary<string, object?>
{ {
["Codec"] = "flac", ["Codec"] = "flac",
["TimeBase"] = "1/44100", ["TimeBase"] = "1/44100",
["DisplayTitle"] = "FLAC - Stereo - Default", ["VideoRange"] = "Unknown",
["VideoRangeType"] = "Unknown",
["AudioSpatialFormat"] = "None",
["LocalizedDefault"] = "Default",
["LocalizedExternal"] = "External",
["DisplayTitle"] = "FLAC - Stereo",
["IsInterlaced"] = false, ["IsInterlaced"] = false,
["IsAVC"] = false,
["ChannelLayout"] = "stereo", ["ChannelLayout"] = "stereo",
["BitRate"] = 1337000, ["BitRate"] = 1337000,
["BitDepth"] = 16,
["Channels"] = 2, ["Channels"] = 2,
["SampleRate"] = 44100, ["SampleRate"] = 44100,
["IsDefault"] = true, ["IsDefault"] = false,
["IsForced"] = false, ["IsForced"] = false,
["IsHearingImpaired"] = false,
["Type"] = "Audio", ["Type"] = "Audio",
["Index"] = 0, ["Index"] = 0,
["IsExternal"] = false, ["IsExternal"] = false,
@@ -366,14 +394,12 @@ public class JellyfinResponseBuilder
["Level"] = 0 ["Level"] = 0
} }
}, },
["MediaAttachments"] = new List<object>(), // Empty array instead of null ["MediaAttachments"] = new List<object>(),
["Formats"] = new List<string>(), // Empty array instead of null ["Formats"] = new List<string>(),
["RequiredHttpHeaders"] = new Dictionary<string, string>(), // Empty dict instead of null ["Bitrate"] = 1337000,
["RunTimeTicks"] = (song.Duration ?? 180) * 10000000L, // Duration in ticks (100ns units) ["RequiredHttpHeaders"] = new Dictionary<string, string>(),
["Name"] = song.Title, ["TranscodingSubProtocol"] = "http",
["AnalyzeDurationMs"] = 0,
["DefaultAudioStreamIndex"] = 0, ["DefaultAudioStreamIndex"] = 0,
["DefaultSubtitleStreamIndex"] = -1,
["HasSegments"] = false ["HasSegments"] = false
} }
}; };
@@ -401,40 +427,68 @@ public class JellyfinResponseBuilder
var item = new Dictionary<string, object?> var item = new Dictionary<string, object?>
{ {
["Id"] = album.Id,
["Name"] = albumName, ["Name"] = albumName,
["ServerId"] = "allstarr", ["ServerId"] = "allstarr",
["Type"] = "MusicAlbum", ["Id"] = album.Id,
["IsFolder"] = true, ["PremiereDate"] = album.Year.HasValue ? $"{album.Year}-01-01T05:00:00.0000000Z" : null,
["AlbumArtist"] = album.Artist,
["AlbumArtists"] = new[]
{
new Dictionary<string, object?>
{
["Id"] = album.ArtistId ?? album.Id,
["Name"] = album.Artist
}
},
["ProductionYear"] = album.Year,
["ChildCount"] = album.SongCount ?? album.Songs.Count,
["ImageTags"] = new Dictionary<string, string>
{
["Primary"] = album.Id
},
["BackdropImageTags"] = new string[0],
["ImageBlurHashes"] = new Dictionary<string, object>(),
["LocationType"] = "FileSystem",
["MediaType"] = (object?)null,
["ChannelId"] = (object?)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<string, object?>
{
["Name"] = album.Genre,
["Id"] = $"genre-{album.Genre?.ToLowerInvariant()}"
}
}
: new Dictionary<string, object?>[0],
["ParentLogoItemId"] = album.ArtistId ?? album.Id,
["ParentBackdropItemId"] = album.ArtistId ?? album.Id,
["ParentBackdropImageTags"] = new string[0],
["UserData"] = new Dictionary<string, object> ["UserData"] = new Dictionary<string, object>
{ {
["PlaybackPositionTicks"] = 0, ["PlaybackPositionTicks"] = 0,
["PlayCount"] = 0, ["PlayCount"] = 0,
["IsFavorite"] = false, ["IsFavorite"] = false,
["Played"] = false, ["Played"] = false,
["Key"] = album.Id ["Key"] = $"{album.Artist}-{album.Title}",
} ["ItemId"] = album.Id
},
["Artists"] = new[] { album.Artist },
["ArtistItems"] = new[]
{
new Dictionary<string, object?>
{
["Name"] = album.Artist,
["Id"] = album.ArtistId ?? album.Id
}
},
["AlbumArtist"] = album.Artist,
["AlbumArtists"] = new[]
{
new Dictionary<string, object?>
{
["Name"] = album.Artist,
["Id"] = album.ArtistId ?? album.Id
}
},
["ImageTags"] = new Dictionary<string, string>
{
["Primary"] = album.Id
},
["BackdropImageTags"] = new string[0],
["ParentLogoImageTag"] = album.ArtistId ?? album.Id,
["ImageBlurHashes"] = new Dictionary<string, object>(),
["LocationType"] = "FileSystem",
["MediaType"] = "Unknown",
["ChildCount"] = album.SongCount ?? album.Songs.Count
}; };
// Add provider IDs for external content // 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; return item;
} }
@@ -468,30 +517,33 @@ public class JellyfinResponseBuilder
var item = new Dictionary<string, object?> var item = new Dictionary<string, object?>
{ {
["Id"] = artist.Id,
["Name"] = artistName, ["Name"] = artistName,
["ServerId"] = "allstarr", ["ServerId"] = "allstarr",
["Type"] = "MusicArtist", ["Id"] = artist.Id,
["ChannelId"] = (object?)null,
["Genres"] = new string[0], // Artists aggregate genres from albums/tracks
["RunTimeTicks"] = 0,
["IsFolder"] = true, ["IsFolder"] = true,
["AlbumCount"] = artist.AlbumCount ?? 0, ["Type"] = "MusicArtist",
["ImageTags"] = new Dictionary<string, string> ["GenreItems"] = new Dictionary<string, object?>[0],
{
["Primary"] = artist.Id
},
["BackdropImageTags"] = new string[0],
["ImageBlurHashes"] = new Dictionary<string, object>(),
["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
["UserData"] = new Dictionary<string, object> ["UserData"] = new Dictionary<string, object>
{ {
["PlaybackPositionTicks"] = 0, ["PlaybackPositionTicks"] = 0,
["PlayCount"] = 0, ["PlayCount"] = 0,
["IsFavorite"] = false, ["IsFavorite"] = false,
["Played"] = false, ["Played"] = false,
["Key"] = artist.Id ["Key"] = $"Artist-{artist.Name}",
} ["ItemId"] = artist.Id
},
["ImageTags"] = new Dictionary<string, string>
{
["Primary"] = artist.Id
},
["BackdropImageTags"] = new string[0],
["ImageBlurHashes"] = new Dictionary<string, object>(),
["LocationType"] = "FileSystem",
["MediaType"] = "Unknown",
["AlbumCount"] = artist.AlbumCount ?? 0
}; };
// Add provider IDs for external content // Add provider IDs for external content
@@ -528,7 +580,7 @@ public class JellyfinResponseBuilder
} }
/// <summary> /// <summary>
/// Converts an ExternalPlaylist to a Jellyfin album item. /// Converts an ExternalPlaylist to a Jellyfin playlist item.
/// </summary> /// </summary>
public Dictionary<string, object?> ConvertPlaylistToJellyfinItem(ExternalPlaylist playlist) public Dictionary<string, object?> ConvertPlaylistToJellyfinItem(ExternalPlaylist playlist)
{ {
@@ -538,13 +590,24 @@ public class JellyfinResponseBuilder
var item = new Dictionary<string, object?> var item = new Dictionary<string, object?>
{ {
["Id"] = playlist.Id,
["Name"] = playlist.Name, ["Name"] = playlist.Name,
["ServerId"] = "allstarr", ["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, ["IsFolder"] = true,
["AlbumArtist"] = curatorName, ["Type"] = "Playlist",
["Genres"] = new[] { "Playlist" }, ["GenreItems"] = new Dictionary<string, object?>[0],
["UserData"] = new Dictionary<string, object>
{
["PlaybackPositionTicks"] = 0,
["PlayCount"] = 0,
["IsFavorite"] = false,
["Played"] = false,
["Key"] = playlist.Id,
["ItemId"] = playlist.Id
},
["ChildCount"] = playlist.TrackCount, ["ChildCount"] = playlist.TrackCount,
["ImageTags"] = new Dictionary<string, string> ["ImageTags"] = new Dictionary<string, string>
{ {
@@ -553,20 +616,10 @@ public class JellyfinResponseBuilder
["BackdropImageTags"] = new string[0], ["BackdropImageTags"] = new string[0],
["ImageBlurHashes"] = new Dictionary<string, object>(), ["ImageBlurHashes"] = new Dictionary<string, object>(),
["LocationType"] = "FileSystem", ["LocationType"] = "FileSystem",
["MediaType"] = (object?)null, ["MediaType"] = "Audio",
["ChannelId"] = (object?)null,
["CollectionType"] = (object?)null,
["ProviderIds"] = new Dictionary<string, string> ["ProviderIds"] = new Dictionary<string, string>
{ {
[playlist.Provider] = playlist.ExternalId [playlist.Provider] = playlist.ExternalId
},
["UserData"] = new Dictionary<string, object>
{
["PlaybackPositionTicks"] = 0,
["PlayCount"] = 0,
["IsFavorite"] = false,
["Played"] = false,
["Key"] = playlist.Id
} }
}; };