diff --git a/server/jellyfin/dto/dto.go b/server/jellyfin/dto/dto.go index 97fdba4b9..f8c249f4e 100644 --- a/server/jellyfin/dto/dto.go +++ b/server/jellyfin/dto/dto.go @@ -179,20 +179,38 @@ type AuthenticationResult struct { ServerId string `json:"ServerId"` } +// MediaSourceInfo mirrors real Jellyfin's MediaSourceInfo object. Finamp's model declares several +// of these bools and arrays as non-nullable, so a missing field deserializes as null and throws a +// "type 'Null' is not a subtype of type 'bool'" cast error that aborts parsing of the whole item +// list. They must always be emitted (no omitempty on bools) even when false/empty. type MediaSourceInfo struct { - Id string `json:"Id"` - Path string `json:"Path,omitempty"` - Protocol string `json:"Protocol"` - Container string `json:"Container,omitempty"` - Size int64 `json:"Size,omitempty"` - Name string `json:"Name,omitempty"` - IsRemote bool `json:"IsRemote"` - RunTimeTicks int64 `json:"RunTimeTicks,omitempty"` - Bitrate int `json:"Bitrate,omitempty"` - SupportsTranscoding bool `json:"SupportsTranscoding"` - SupportsDirectStream bool `json:"SupportsDirectStream"` - SupportsDirectPlay bool `json:"SupportsDirectPlay"` - Type string `json:"Type"` + Id string `json:"Id"` + Path string `json:"Path,omitempty"` + Protocol string `json:"Protocol"` + Container string `json:"Container,omitempty"` + Size int64 `json:"Size,omitempty"` + Name string `json:"Name,omitempty"` + IsRemote bool `json:"IsRemote"` + RunTimeTicks int64 `json:"RunTimeTicks,omitempty"` + Bitrate int `json:"Bitrate,omitempty"` + SupportsTranscoding bool `json:"SupportsTranscoding"` + SupportsDirectStream bool `json:"SupportsDirectStream"` + SupportsDirectPlay bool `json:"SupportsDirectPlay"` + Type string `json:"Type"` + ReadAtNativeFramerate bool `json:"ReadAtNativeFramerate"` + IgnoreDts bool `json:"IgnoreDts"` + IgnoreIndex bool `json:"IgnoreIndex"` + GenPtsInput bool `json:"GenPtsInput"` + IsInfiniteStream bool `json:"IsInfiniteStream"` + UseMostCompatibleTranscodingProfile bool `json:"UseMostCompatibleTranscodingProfile"` + RequiresOpening bool `json:"RequiresOpening"` + RequiresClosing bool `json:"RequiresClosing"` + RequiresLooping bool `json:"RequiresLooping"` + SupportsProbing bool `json:"SupportsProbing"` + HasSegments bool `json:"HasSegments"` + MediaStreams []any `json:"MediaStreams"` + MediaAttachments []any `json:"MediaAttachments"` + Formats []string `json:"Formats"` } type PlaybackInfoResponse struct { diff --git a/server/jellyfin/dto/mappers.go b/server/jellyfin/dto/mappers.go index 4f62d3acd..4157b5c2c 100644 --- a/server/jellyfin/dto/mappers.go +++ b/server/jellyfin/dto/mappers.go @@ -26,6 +26,10 @@ func MediaSourceFromMediaFile(mf model.MediaFile) MediaSourceInfo { SupportsDirectStream: true, SupportsTranscoding: true, IsRemote: false, + SupportsProbing: true, + MediaStreams: []any{}, + MediaAttachments: []any{}, + Formats: []string{}, } } diff --git a/server/jellyfin/dto/mappers_test.go b/server/jellyfin/dto/mappers_test.go index 1a63ed47c..53ac024e5 100644 --- a/server/jellyfin/dto/mappers_test.go +++ b/server/jellyfin/dto/mappers_test.go @@ -1,6 +1,7 @@ package dto import ( + "encoding/json" "time" "github.com/navidrome/navidrome/model" @@ -44,6 +45,20 @@ var _ = Describe("mappers", func() { Expect(src.SupportsDirectPlay).To(BeTrue()) }) + It("serializes all Finamp-required MediaSourceInfo bools and arrays, never as null", func() { + mf := model.MediaFile{ID: "s1", Size: 5242880, Suffix: "mp3", BitRate: 320, Duration: 100} + src := MediaSourceFromMediaFile(mf) + b, err := json.Marshal(src) + Expect(err).ToNot(HaveOccurred()) + j := string(b) + Expect(j).To(ContainSubstring(`"SupportsProbing":true`)) + Expect(j).To(ContainSubstring(`"IsInfiniteStream":false`)) + Expect(j).To(ContainSubstring(`"RequiresOpening":false`)) + Expect(j).To(ContainSubstring(`"MediaStreams":[]`)) + Expect(j).To(ContainSubstring(`"MediaAttachments":[]`)) + Expect(j).To(ContainSubstring(`"Formats":[]`)) + }) + It("omits IndexNumber and ParentIndexNumber when track/disc numbers are untagged", func() { mf := model.MediaFile{ ID: "song-2", Title: "Song", Album: "Alb", AlbumID: "alb-1",