mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-09 11:48:41 +01:00
fix(jellyfin): populate MediaStreams with the audio stream so clients can size/transcode
This commit is contained in:
parent
1703b59d24
commit
c70d3c17e6
3 changed files with 104 additions and 31 deletions
|
|
@ -179,38 +179,58 @@ type AuthenticationResult struct {
|
|||
ServerId string `json:"ServerId"`
|
||||
}
|
||||
|
||||
// MediaStream mirrors real Jellyfin's MediaStream object. Finamp's model declares several bools
|
||||
// as non-nullable, so they must always be emitted (no omitempty) even when false. Finamp's
|
||||
// MediaSourceInfo.transcodedSize also does MediaStreams.firstWhere((s) => s.type == 'Audio'), so
|
||||
// MediaSourceInfo must always include at least one Audio stream or that lookup throws.
|
||||
type MediaStream struct {
|
||||
Codec string `json:"Codec,omitempty"`
|
||||
Type string `json:"Type"`
|
||||
Index int `json:"Index"`
|
||||
BitRate int `json:"BitRate,omitempty"`
|
||||
Channels int `json:"Channels,omitempty"`
|
||||
SampleRate int `json:"SampleRate,omitempty"`
|
||||
ChannelLayout string `json:"ChannelLayout,omitempty"`
|
||||
IsInterlaced bool `json:"IsInterlaced"`
|
||||
IsDefault bool `json:"IsDefault"`
|
||||
IsForced bool `json:"IsForced"`
|
||||
IsExternal bool `json:"IsExternal"`
|
||||
IsTextSubtitleStream bool `json:"IsTextSubtitleStream"`
|
||||
SupportsExternalStream bool `json:"SupportsExternalStream"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
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 []MediaStream `json:"MediaStreams"`
|
||||
MediaAttachments []any `json:"MediaAttachments"`
|
||||
Formats []string `json:"Formats"`
|
||||
}
|
||||
|
||||
type PlaybackInfoResponse struct {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,22 @@ import (
|
|||
|
||||
func TicksFromSeconds(sec float32) int64 { return int64(float64(sec) * 1e7) }
|
||||
|
||||
// channelLayout maps a channel count to the label Jellyfin clients expect on a MediaStream.
|
||||
func channelLayout(n int) string {
|
||||
switch n {
|
||||
case 1:
|
||||
return "mono"
|
||||
case 2:
|
||||
return "stereo"
|
||||
case 6:
|
||||
return "5.1"
|
||||
case 8:
|
||||
return "7.1"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// MediaSourceFromMediaFile builds the MediaSourceInfo describing direct playback of mf's
|
||||
// source file. Shared by SongToBaseItem and getPlaybackInfo so clients see the same Size and
|
||||
// Bitrate whether they read it from a browse response or from /PlaybackInfo -- Finamp's
|
||||
|
|
@ -27,9 +43,17 @@ func MediaSourceFromMediaFile(mf model.MediaFile) MediaSourceInfo {
|
|||
SupportsTranscoding: true,
|
||||
IsRemote: false,
|
||||
SupportsProbing: true,
|
||||
MediaStreams: []any{},
|
||||
MediaAttachments: []any{},
|
||||
Formats: []string{},
|
||||
MediaStreams: []MediaStream{{
|
||||
Type: "Audio",
|
||||
Index: 0,
|
||||
Codec: mf.Codec,
|
||||
BitRate: mf.BitRate * 1000, // Navidrome stores kbps; Jellyfin's BitRate is bps.
|
||||
Channels: mf.Channels,
|
||||
SampleRate: mf.SampleRate,
|
||||
ChannelLayout: channelLayout(mf.Channels),
|
||||
}},
|
||||
MediaAttachments: []any{},
|
||||
Formats: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,22 @@ var _ = Describe("mappers", func() {
|
|||
Expect(src.SupportsDirectPlay).To(BeTrue())
|
||||
})
|
||||
|
||||
It("populates MediaStreams with a single Audio stream so Finamp can size downloads", func() {
|
||||
mf := model.MediaFile{
|
||||
ID: "s1", Size: 5242880, Suffix: "mp3", BitRate: 320, Duration: 100,
|
||||
Channels: 2, SampleRate: 44100, Codec: "mp3",
|
||||
}
|
||||
src := MediaSourceFromMediaFile(mf)
|
||||
Expect(src.MediaStreams).To(HaveLen(1))
|
||||
stream := src.MediaStreams[0]
|
||||
Expect(stream.Type).To(Equal("Audio"))
|
||||
Expect(stream.Channels).To(Equal(2))
|
||||
Expect(stream.SampleRate).To(Equal(44100))
|
||||
Expect(stream.BitRate).To(Equal(320_000))
|
||||
Expect(stream.Codec).To(Equal("mp3"))
|
||||
Expect(stream.ChannelLayout).To(Equal("stereo"))
|
||||
})
|
||||
|
||||
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)
|
||||
|
|
@ -56,11 +72,24 @@ var _ = Describe("mappers", func() {
|
|||
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("serializes MediaStream's required non-nullable bools, never omitted", func() {
|
||||
stream := MediaStream{Type: "Audio", Index: 0}
|
||||
b, err := json.Marshal(stream)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
j := string(b)
|
||||
Expect(j).To(ContainSubstring(`"Type":"Audio"`))
|
||||
Expect(j).To(ContainSubstring(`"IsDefault":false`))
|
||||
Expect(j).To(ContainSubstring(`"IsInterlaced":false`))
|
||||
Expect(j).To(ContainSubstring(`"IsForced":false`))
|
||||
Expect(j).To(ContainSubstring(`"IsExternal":false`))
|
||||
Expect(j).To(ContainSubstring(`"IsTextSubtitleStream":false`))
|
||||
Expect(j).To(ContainSubstring(`"SupportsExternalStream":false`))
|
||||
})
|
||||
|
||||
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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue