diff --git a/server/jellyfin/dto/dto.go b/server/jellyfin/dto/dto.go index 2a817739b..c8f7129cb 100644 --- a/server/jellyfin/dto/dto.go +++ b/server/jellyfin/dto/dto.go @@ -61,20 +61,22 @@ type BaseItemDto struct { PremiereDate *string `json:"PremiereDate,omitempty"` // DateCreated is the ISO 8601 date the item was added to the library; clients show it as // "Date Added" and sort "Recently Added" by it. - DateCreated string `json:"DateCreated,omitempty"` - Album string `json:"Album,omitempty"` - AlbumId string `json:"AlbumId,omitempty"` - AlbumArtist string `json:"AlbumArtist,omitempty"` - AlbumArtists []NameGuidPair `json:"AlbumArtists,omitempty"` - AlbumPrimaryImageTag string `json:"AlbumPrimaryImageTag,omitempty"` - Artists []string `json:"Artists,omitempty"` - ArtistItems []NameGuidPair `json:"ArtistItems,omitempty"` - Genres []string `json:"Genres,omitempty"` - GenreItems []NameGuidPair `json:"GenreItems,omitempty"` - ChildCount *int `json:"ChildCount,omitempty"` - SongCount *int `json:"SongCount,omitempty"` - AlbumCount *int `json:"AlbumCount,omitempty"` - ImageTags map[string]string `json:"ImageTags,omitempty"` + DateCreated string `json:"DateCreated,omitempty"` + Album string `json:"Album,omitempty"` + AlbumId string `json:"AlbumId,omitempty"` + AlbumArtist string `json:"AlbumArtist,omitempty"` + AlbumArtists []NameGuidPair `json:"AlbumArtists,omitempty"` + AlbumPrimaryImageTag string `json:"AlbumPrimaryImageTag,omitempty"` + Artists []string `json:"Artists,omitempty"` + ArtistItems []NameGuidPair `json:"ArtistItems,omitempty"` + Genres []string `json:"Genres,omitempty"` + GenreItems []NameGuidPair `json:"GenreItems,omitempty"` + NormalizationGain *float64 `json:"NormalizationGain,omitempty"` + AlbumNormalizationGain *float64 `json:"AlbumNormalizationGain,omitempty"` + ChildCount *int `json:"ChildCount,omitempty"` + SongCount *int `json:"SongCount,omitempty"` + AlbumCount *int `json:"AlbumCount,omitempty"` + ImageTags map[string]string `json:"ImageTags,omitempty"` // ImageBlurHashes is keyed by image type (e.g. "Primary") then image tag. Finamp uses it as a // de-dup key for image downloads (and a placeholder); absent, it warns the server isn't // calculating blurhashes. diff --git a/server/jellyfin/dto/mappers.go b/server/jellyfin/dto/mappers.go index dc9bc5fbd..bdb0a1e37 100644 --- a/server/jellyfin/dto/mappers.go +++ b/server/jellyfin/dto/mappers.go @@ -171,6 +171,9 @@ func SongToBaseItem(mf model.MediaFile, fields Fields) BaseItemDto { if mf.AlbumArtistID != "" { item.AlbumArtists = []NameGuidPair{{Name: mf.AlbumArtist, Id: EncodeID(mf.AlbumArtistID)}} } + // dB to apply at the RG2 -18 LUFS reference, same convention real Jellyfin uses; no conversion. + item.NormalizationGain = mf.RGTrackGain + item.AlbumNormalizationGain = mf.RGAlbumGain if mf.Year > 0 { item.ProductionYear = new(mf.Year) } diff --git a/server/jellyfin/dto/mappers_test.go b/server/jellyfin/dto/mappers_test.go index 6bfc9d95a..8867a5e73 100644 --- a/server/jellyfin/dto/mappers_test.go +++ b/server/jellyfin/dto/mappers_test.go @@ -125,6 +125,22 @@ var _ = Describe("mappers", func() { Expect(item.AlbumArtists).To(Equal([]NameGuidPair{{Name: "De La Soul", Id: EncodeID("ar-delasoul")}})) }) + It("serializes normalization gains with Jellyfin's exact key casing", func() { + mf := model.MediaFile{ID: "s1", Title: "Song", + RGTrackGain: new(-3.5), RGAlbumGain: new(-4.25)} + b, err := json.Marshal(SongToBaseItem(mf, nil)) + Expect(err).ToNot(HaveOccurred()) + Expect(string(b)).To(ContainSubstring(`"NormalizationGain":-3.5`)) + Expect(string(b)).To(ContainSubstring(`"AlbumNormalizationGain":-4.25`)) + }) + + It("omits normalization gains when the file has no ReplayGain tags", func() { + b, err := json.Marshal(SongToBaseItem(model.MediaFile{ID: "s1", Title: "Song"}, nil)) + Expect(err).ToNot(HaveOccurred()) + // Substring check covers both keys (AlbumNormalizationGain contains NormalizationGain). + Expect(string(b)).ToNot(ContainSubstring("NormalizationGain")) + }) + 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) diff --git a/server/jellyfin/e2e/browsing_test.go b/server/jellyfin/e2e/browsing_test.go index 382ec9406..a0632994e 100644 --- a/server/jellyfin/e2e/browsing_test.go +++ b/server/jellyfin/e2e/browsing_test.go @@ -415,6 +415,22 @@ var _ = Describe("Browsing", func() { Expect(item.AlbumArtists[0].Name).To(Equal("Miles Davis")) }) + It("exposes NormalizationGain and AlbumNormalizationGain from ReplayGain tags", func() { + var item dto.BaseItemDto + parseInto(get("/Items/"+enc(songID("Stairway To Heaven"))), &item) + Expect(item.NormalizationGain).ToNot(BeNil()) + Expect(*item.NormalizationGain).To(BeNumerically("~", -3.5, 0.001)) + Expect(item.AlbumNormalizationGain).ToNot(BeNil()) + Expect(*item.AlbumNormalizationGain).To(BeNumerically("~", -4.25, 0.001)) + }) + + It("omits normalization gains for files without ReplayGain tags", func() { + var item dto.BaseItemDto + parseInto(get("/Items/"+enc(songID("So What"))), &item) + Expect(item.NormalizationGain).To(BeNil()) + Expect(item.AlbumNormalizationGain).To(BeNil()) + }) + It("resolves an artist", func() { var item dto.BaseItemDto parseInto(get("/Items/"+enc(artistID("Miles Davis"))), &item) diff --git a/server/jellyfin/e2e/e2e_suite_test.go b/server/jellyfin/e2e/e2e_suite_test.go index 445566387..5a4c8c031 100644 --- a/server/jellyfin/e2e/e2e_suite_test.go +++ b/server/jellyfin/e2e/e2e_suite_test.go @@ -121,7 +121,9 @@ func buildTestFS() storagetest.FakeFS { "Rock/The Beatles/Abbey Road/02 - Come Together.mp3": abbeyRoad(track(2, "Come Together")), "Rock/The Beatles/Help!/01 - Help.mp3": help(track(1, "Help!")), "Rock/Led Zeppelin/IV/01 - Stairway To Heaven.mp3": ledZepIV(track(1, "Stairway To Heaven", _t{ - "lyrics:eng": "[00:01.00]There's a lady who's sure\n[00:05.50]All that glitters is gold", + "lyrics:eng": "[00:01.00]There's a lady who's sure\n[00:05.50]All that glitters is gold", + "replaygain_track_gain": "-3.50 dB", + "replaygain_album_gain": "-4.25 dB", })), "Jazz/Miles Davis/Kind of Blue/01 - So What.mp3": kindOfBlue(track(1, "So What")), "Pop/Solo Artist/Singles/01 - Standalone Track.mp3": singles(track(1, "Standalone Track")),