fix(jellyfin): include MediaSources with Size/Bitrate on tracks so clients show download size

This commit is contained in:
Deluan 2026-07-05 21:31:06 -04:00
commit 200ae482c3
5 changed files with 40 additions and 13 deletions

View file

@ -188,6 +188,7 @@ type MediaSourceInfo struct {
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"`

View file

@ -8,6 +8,27 @@ import (
func TicksFromSeconds(sec float32) int64 { return int64(float64(sec) * 1e7) }
// 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
// download dialog reads MediaSources[0].Size from the former.
func MediaSourceFromMediaFile(mf model.MediaFile) MediaSourceInfo {
return MediaSourceInfo{
Id: mf.ID,
Protocol: "Http",
Container: mf.Suffix,
Size: mf.Size,
Name: mf.Title,
Type: "Default",
RunTimeTicks: TicksFromSeconds(mf.Duration),
Bitrate: mf.BitRate * 1000, // Navidrome stores kbps; Jellyfin's Bitrate is bps.
SupportsDirectPlay: true,
SupportsDirectStream: true,
SupportsTranscoding: true,
IsRemote: false,
}
}
func UserData(a model.Annotations, itemID string) *UserItemDataDto {
d := &UserItemDataDto{
PlayCount: int(a.PlayCount),
@ -44,6 +65,7 @@ func SongToBaseItem(mf model.MediaFile) BaseItemDto {
CanDownload: true,
BackdropImageTags: []string{},
UserData: UserData(mf.Annotations, mf.ID),
MediaSources: []MediaSourceInfo{MediaSourceFromMediaFile(mf)},
}
if mf.Year > 0 {
item.ProductionYear = new(mf.Year)

View file

@ -13,7 +13,7 @@ var _ = Describe("mappers", func() {
mf := model.MediaFile{
ID: "song-1", Title: "Song", Album: "Alb", AlbumID: "alb-1",
Artist: "Art", AlbumArtist: "AA", TrackNumber: 3, DiscNumber: 1,
Year: 1999, Duration: 60,
Year: 1999, Duration: 60, Size: 2_500_000,
}
mf.PlayCount = 2
mf.Starred = true
@ -28,6 +28,20 @@ var _ = Describe("mappers", func() {
Expect(item.UserData.IsFavorite).To(BeTrue())
Expect(item.UserData.PlayCount).To(Equal(2))
Expect(item.UserData.Played).To(BeTrue())
Expect(item.MediaSources).To(HaveLen(1))
Expect(item.MediaSources[0].Size).To(Equal(int64(2_500_000)))
})
It("builds a MediaSourceInfo from a media file", func() {
mf := model.MediaFile{ID: "s1", Size: 5242880, Suffix: "mp3", BitRate: 320, Duration: 100}
src := MediaSourceFromMediaFile(mf)
Expect(src.Id).To(Equal("s1"))
Expect(src.Size).To(Equal(int64(5242880)))
Expect(src.Container).To(Equal("mp3"))
Expect(src.Bitrate).To(Equal(320_000))
Expect(src.RunTimeTicks).To(Equal(int64(1_000_000_000)))
Expect(src.Protocol).To(Equal("Http"))
Expect(src.SupportsDirectPlay).To(BeTrue())
})
It("omits IndexNumber and ParentIndexNumber when track/disc numbers are untagged", func() {

View file

@ -40,18 +40,7 @@ func (api *Router) getPlaybackInfo(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
src := dto.MediaSourceInfo{
Id: mf.ID,
Protocol: "Http",
Container: mf.Suffix,
Size: mf.Size,
Name: mf.Title,
Type: "Default",
RunTimeTicks: dto.TicksFromSeconds(mf.Duration),
SupportsDirectPlay: true,
SupportsDirectStream: true,
SupportsTranscoding: true,
}
src := dto.MediaSourceFromMediaFile(*mf)
api.ok(w, r, dto.PlaybackInfoResponse{MediaSources: []dto.MediaSourceInfo{src}, PlaySessionId: mf.ID})
}

View file

@ -52,6 +52,7 @@ var _ = Describe("Stream", func() {
Expect(res.MediaSources).To(HaveLen(1))
Expect(res.MediaSources[0].Id).To(Equal("s1"))
Expect(res.MediaSources[0].Container).To(Equal("mp3"))
Expect(res.MediaSources[0].Size).To(Equal(int64(1000)))
Expect(res.PlaySessionId).ToNot(BeEmpty())
})