fix: Allow nullable ReplayGain and support 0.0 (#4239)

* fix(ui,scanner,subsonic): Allow nullable replaygain and support 0.0

Resolves #4236.

Makes the replaygain columns (track/album gain/peak) nullable.
Converts the type to a pointer, allowing for 0.0 (a valid value) to be returned from Subsonic.
Updates tests for this behavior.

* small refactor

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Kendall Garner 2025-06-17 16:02:25 +00:00 committed by GitHub
parent 4359adc042
commit 7640c474cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 279 additions and 96 deletions

View file

@ -8,6 +8,7 @@ import (
"github.com/djherbis/times"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/metadata"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -82,6 +83,29 @@ var _ = Describe("Extractor", func() {
e = &extractor{}
})
Describe("ReplayGain", func() {
DescribeTable("test replaygain end-to-end", func(file string, trackGain, trackPeak, albumGain, albumPeak *float64) {
path := "tests/fixtures/" + file
mds, err := e.Parse(path)
Expect(err).ToNot(HaveOccurred())
info := mds[path]
fileInfo, _ := os.Stat(path)
info.FileInfo = testFileInfo{FileInfo: fileInfo}
metadata := metadata.New(path, info)
mf := metadata.ToMediaFile(1, "folderID")
Expect(mf.RGTrackGain).To(Equal(trackGain))
Expect(mf.RGTrackPeak).To(Equal(trackPeak))
Expect(mf.RGAlbumGain).To(Equal(albumGain))
Expect(mf.RGAlbumPeak).To(Equal(albumPeak))
},
Entry("mp3 with no replaygain", "no_replaygain.mp3", nil, nil, nil, nil),
Entry("mp3 with no zero replaygain", "zero_replaygain.mp3", gg.P(0.0), gg.P(1.0), gg.P(0.0), gg.P(1.0)),
)
})
Describe("Participants", func() {
DescribeTable("test tags consistent across formats", func(format string) {
path := "tests/fixtures/test." + format

View file

@ -0,0 +1,49 @@
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(upMakeReplaygainFieldsNullable, downMakeReplaygainFieldsNullable)
}
func upMakeReplaygainFieldsNullable(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
ALTER TABLE media_file ADD COLUMN rg_album_gain_new real;
ALTER TABLE media_file ADD COLUMN rg_album_peak_new real;
ALTER TABLE media_file ADD COLUMN rg_track_gain_new real;
ALTER TABLE media_file ADD COLUMN rg_track_peak_new real;
UPDATE media_file SET
rg_album_gain_new = rg_album_gain,
rg_album_peak_new = rg_album_peak,
rg_track_gain_new = rg_track_gain,
rg_track_peak_new = rg_track_peak;
ALTER TABLE media_file DROP COLUMN rg_album_gain;
ALTER TABLE media_file DROP COLUMN rg_album_peak;
ALTER TABLE media_file DROP COLUMN rg_track_gain;
ALTER TABLE media_file DROP COLUMN rg_track_peak;
ALTER TABLE media_file RENAME COLUMN rg_album_gain_new TO rg_album_gain;
ALTER TABLE media_file RENAME COLUMN rg_album_peak_new TO rg_album_peak;
ALTER TABLE media_file RENAME COLUMN rg_track_gain_new TO rg_track_gain;
ALTER TABLE media_file RENAME COLUMN rg_track_peak_new TO rg_track_peak;
`)
if err != nil {
return err
}
notice(tx, "Fetching replaygain fields properly will require a full scan")
return nil
}
func downMakeReplaygainFieldsNullable(ctx context.Context, tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

View file

@ -36,53 +36,53 @@ type MediaFile struct {
Artist string `structs:"artist" json:"artist"`
AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"` // Deprecated: Use Participants instead
// AlbumArtist is the display name used for the album artist.
AlbumArtist string `structs:"album_artist" json:"albumArtist"`
AlbumID string `structs:"album_id" json:"albumId"`
HasCoverArt bool `structs:"has_cover_art" json:"hasCoverArt"`
TrackNumber int `structs:"track_number" json:"trackNumber"`
DiscNumber int `structs:"disc_number" json:"discNumber"`
DiscSubtitle string `structs:"disc_subtitle" json:"discSubtitle,omitempty"`
Year int `structs:"year" json:"year"`
Date string `structs:"date" json:"date,omitempty"`
OriginalYear int `structs:"original_year" json:"originalYear"`
OriginalDate string `structs:"original_date" json:"originalDate,omitempty"`
ReleaseYear int `structs:"release_year" json:"releaseYear"`
ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"`
Size int64 `structs:"size" json:"size"`
Suffix string `structs:"suffix" json:"suffix"`
Duration float32 `structs:"duration" json:"duration"`
BitRate int `structs:"bit_rate" json:"bitRate"`
SampleRate int `structs:"sample_rate" json:"sampleRate"`
BitDepth int `structs:"bit_depth" json:"bitDepth"`
Channels int `structs:"channels" json:"channels"`
Genre string `structs:"genre" json:"genre"`
Genres Genres `structs:"-" json:"genres,omitempty"`
SortTitle string `structs:"sort_title" json:"sortTitle,omitempty"`
SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"`
SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"` // Deprecated: Use Participants instead
SortAlbumArtistName string `structs:"sort_album_artist_name" json:"sortAlbumArtistName,omitempty"` // Deprecated: Use Participants instead
OrderTitle string `structs:"order_title" json:"orderTitle,omitempty"`
OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"`
OrderArtistName string `structs:"order_artist_name" json:"orderArtistName"` // Deprecated: Use Participants instead
OrderAlbumArtistName string `structs:"order_album_artist_name" json:"orderAlbumArtistName"` // Deprecated: Use Participants instead
Compilation bool `structs:"compilation" json:"compilation"`
Comment string `structs:"comment" json:"comment,omitempty"`
Lyrics string `structs:"lyrics" json:"lyrics"`
BPM int `structs:"bpm" json:"bpm,omitempty"`
ExplicitStatus string `structs:"explicit_status" json:"explicitStatus"`
CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"`
MbzRecordingID string `structs:"mbz_recording_id" json:"mbzRecordingID,omitempty"`
MbzReleaseTrackID string `structs:"mbz_release_track_id" json:"mbzReleaseTrackId,omitempty"`
MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty"`
MbzReleaseGroupID string `structs:"mbz_release_group_id" json:"mbzReleaseGroupId,omitempty"`
MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty"` // Deprecated: Use Participants instead
MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty"` // Deprecated: Use Participants instead
MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"`
MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"`
RGAlbumGain float64 `structs:"rg_album_gain" json:"rgAlbumGain"`
RGAlbumPeak float64 `structs:"rg_album_peak" json:"rgAlbumPeak"`
RGTrackGain float64 `structs:"rg_track_gain" json:"rgTrackGain"`
RGTrackPeak float64 `structs:"rg_track_peak" json:"rgTrackPeak"`
AlbumArtist string `structs:"album_artist" json:"albumArtist"`
AlbumID string `structs:"album_id" json:"albumId"`
HasCoverArt bool `structs:"has_cover_art" json:"hasCoverArt"`
TrackNumber int `structs:"track_number" json:"trackNumber"`
DiscNumber int `structs:"disc_number" json:"discNumber"`
DiscSubtitle string `structs:"disc_subtitle" json:"discSubtitle,omitempty"`
Year int `structs:"year" json:"year"`
Date string `structs:"date" json:"date,omitempty"`
OriginalYear int `structs:"original_year" json:"originalYear"`
OriginalDate string `structs:"original_date" json:"originalDate,omitempty"`
ReleaseYear int `structs:"release_year" json:"releaseYear"`
ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"`
Size int64 `structs:"size" json:"size"`
Suffix string `structs:"suffix" json:"suffix"`
Duration float32 `structs:"duration" json:"duration"`
BitRate int `structs:"bit_rate" json:"bitRate"`
SampleRate int `structs:"sample_rate" json:"sampleRate"`
BitDepth int `structs:"bit_depth" json:"bitDepth"`
Channels int `structs:"channels" json:"channels"`
Genre string `structs:"genre" json:"genre"`
Genres Genres `structs:"-" json:"genres,omitempty"`
SortTitle string `structs:"sort_title" json:"sortTitle,omitempty"`
SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"`
SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"` // Deprecated: Use Participants instead
SortAlbumArtistName string `structs:"sort_album_artist_name" json:"sortAlbumArtistName,omitempty"` // Deprecated: Use Participants instead
OrderTitle string `structs:"order_title" json:"orderTitle,omitempty"`
OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"`
OrderArtistName string `structs:"order_artist_name" json:"orderArtistName"` // Deprecated: Use Participants instead
OrderAlbumArtistName string `structs:"order_album_artist_name" json:"orderAlbumArtistName"` // Deprecated: Use Participants instead
Compilation bool `structs:"compilation" json:"compilation"`
Comment string `structs:"comment" json:"comment,omitempty"`
Lyrics string `structs:"lyrics" json:"lyrics"`
BPM int `structs:"bpm" json:"bpm,omitempty"`
ExplicitStatus string `structs:"explicit_status" json:"explicitStatus"`
CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"`
MbzRecordingID string `structs:"mbz_recording_id" json:"mbzRecordingID,omitempty"`
MbzReleaseTrackID string `structs:"mbz_release_track_id" json:"mbzReleaseTrackId,omitempty"`
MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty"`
MbzReleaseGroupID string `structs:"mbz_release_group_id" json:"mbzReleaseGroupId,omitempty"`
MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty"` // Deprecated: Use Participants instead
MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty"` // Deprecated: Use Participants instead
MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"`
MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"`
RGAlbumGain *float64 `structs:"rg_album_gain" json:"rgAlbumGain"`
RGAlbumPeak *float64 `structs:"rg_album_peak" json:"rgAlbumPeak"`
RGTrackGain *float64 `structs:"rg_track_gain" json:"rgTrackGain"`
RGTrackPeak *float64 `structs:"rg_track_peak" json:"rgTrackPeak"`
Tags Tags `structs:"tags" json:"tags,omitempty" hash:"ignore"` // All imported tags from the original file
Participants Participants `structs:"participants" json:"participants" hash:"ignore"` // All artists that participated in this track

View file

@ -53,9 +53,9 @@ func (md Metadata) ToMediaFile(libID int, folderID string) model.MediaFile {
mf.MbzAlbumType = md.String(model.TagReleaseType)
// ReplayGain
mf.RGAlbumPeak = md.Float(model.TagReplayGainAlbumPeak, 1)
mf.RGAlbumPeak = md.NullableFloat(model.TagReplayGainAlbumPeak)
mf.RGAlbumGain = md.mapGain(model.TagReplayGainAlbumGain, model.TagR128AlbumGain)
mf.RGTrackPeak = md.Float(model.TagReplayGainTrackPeak, 1)
mf.RGTrackPeak = md.NullableFloat(model.TagReplayGainTrackPeak)
mf.RGTrackGain = md.mapGain(model.TagReplayGainTrackGain, model.TagR128TrackGain)
// General properties
@ -108,23 +108,24 @@ func (md Metadata) AlbumID(mf model.MediaFile, pidConf string) string {
return getPID(mf, md, pidConf)
}
func (md Metadata) mapGain(rg, r128 model.TagName) float64 {
func (md Metadata) mapGain(rg, r128 model.TagName) *float64 {
v := md.Gain(rg)
if v != 0 {
if v != nil {
return v
}
r128value := md.String(r128)
if r128value != "" {
var v, err = strconv.Atoi(r128value)
if err != nil {
return 0
return nil
}
// Convert Q7.8 to float
var value = float64(v) / 256.0
value := float64(v) / 256.0
// Adding 5 dB to normalize with ReplayGain level
return value + 5
value += 5
return &value
}
return 0
return nil
}
func (md Metadata) mapLyrics() string {

View file

@ -103,9 +103,11 @@ func (md Metadata) NumAndTotal(key model.TagName) (int, int) { return md.tuple(k
func (md Metadata) Float(key model.TagName, def ...float64) float64 {
return float(md.first(key), def...)
}
func (md Metadata) Gain(key model.TagName) float64 {
func (md Metadata) NullableFloat(key model.TagName) *float64 { return nullableFloat(md.first(key)) }
func (md Metadata) Gain(key model.TagName) *float64 {
v := strings.TrimSpace(strings.Replace(md.first(key), "dB", "", 1))
return float(v)
return nullableFloat(v)
}
func (md Metadata) Pairs(key model.TagName) []Pair {
values := md.tags[key]
@ -119,14 +121,22 @@ func (md Metadata) first(key model.TagName) string {
}
func float(value string, def ...float64) float64 {
v := nullableFloat(value)
if v != nil {
return *v
}
if len(def) > 0 {
return def[0]
}
return 0
}
func nullableFloat(value string) *float64 {
v, err := strconv.ParseFloat(value, 64)
if err != nil || v == math.Inf(-1) || math.IsInf(v, 1) || math.IsNaN(v) {
if len(def) > 0 {
return def[0]
}
return 0
return nil
}
return v
return &v
}
// Used for tracks and discs

View file

@ -8,6 +8,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/metadata"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -257,38 +258,39 @@ var _ = Describe("Metadata", func() {
}
DescribeTable("Gain",
func(tagValue string, expected float64) {
func(tagValue string, expected *float64) {
mf := createMF("replaygain_track_gain", tagValue)
Expect(mf.RGTrackGain).To(Equal(expected))
},
Entry("0", "0", 0.0),
Entry("1.2dB", "1.2dB", 1.2),
Entry("Infinity", "Infinity", 0.0),
Entry("Invalid value", "INVALID VALUE", 0.0),
Entry("NaN", "NaN", 0.0),
Entry("0", "0", gg.P(0.0)),
Entry("1.2dB", "1.2dB", gg.P(1.2)),
Entry("Infinity", "Infinity", nil),
Entry("Invalid value", "INVALID VALUE", nil),
Entry("NaN", "NaN", nil),
)
DescribeTable("Peak",
func(tagValue string, expected float64) {
func(tagValue string, expected *float64) {
mf := createMF("replaygain_track_peak", tagValue)
Expect(mf.RGTrackPeak).To(Equal(expected))
},
Entry("0", "0", 0.0),
Entry("0.5", "0.5", 0.5),
Entry("Invalid dB suffix", "0.7dB", 1.0),
Entry("Infinity", "Infinity", 1.0),
Entry("Invalid value", "INVALID VALUE", 1.0),
Entry("NaN", "NaN", 1.0),
Entry("0", "0", gg.P(0.0)),
Entry("1.0", "1.0", gg.P(1.0)),
Entry("0.5", "0.5", gg.P(0.5)),
Entry("Invalid dB suffix", "0.7dB", nil),
Entry("Infinity", "Infinity", nil),
Entry("Invalid value", "INVALID VALUE", nil),
Entry("NaN", "NaN", nil),
)
DescribeTable("getR128GainValue",
func(tagValue string, expected float64) {
func(tagValue string, expected *float64) {
mf := createMF("r128_track_gain", tagValue)
Expect(mf.RGTrackGain).To(Equal(expected))
},
Entry("0", "0", 5.0),
Entry("-3776", "-3776", -9.75),
Entry("Infinity", "Infinity", 0.0),
Entry("Invalid value", "INVALID VALUE", 0.0),
Entry("0", "0", gg.P(5.0)),
Entry("-3776", "-3776", gg.P(-9.75)),
Entry("Infinity", "Infinity", nil),
Entry("Invalid value", "INVALID VALUE", nil),
)
})

View file

@ -25,10 +25,10 @@ type dbMediaFile struct {
Tags string `structs:"-" json:"-"`
// These are necessary to map the correct names (rg_*) to the correct fields (RG*)
// without using `db` struct tags in the model.MediaFile struct
RgAlbumGain float64 `structs:"-" json:"-"`
RgAlbumPeak float64 `structs:"-" json:"-"`
RgTrackGain float64 `structs:"-" json:"-"`
RgTrackPeak float64 `structs:"-" json:"-"`
RgAlbumGain *float64 `structs:"-" json:"-"`
RgAlbumPeak *float64 `structs:"-" json:"-"`
RgTrackGain *float64 `structs:"-" json:"-"`
RgTrackPeak *float64 `structs:"-" json:"-"`
}
func (m *dbMediaFile) PostScan() error {

View file

@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/tests"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/pocketbase/dbx"
@ -79,7 +80,7 @@ var (
songAntenna = mf(model.MediaFile{ID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk",
AlbumID: "103",
Path: p("/kraft/radio/antenna.mp3"),
RGAlbumGain: 1.0, RGAlbumPeak: 2.0, RGTrackGain: 3.0, RGTrackPeak: 4.0,
RGAlbumGain: gg.P(1.0), RGAlbumPeak: gg.P(2.0), RGTrackGain: gg.P(3.0), RGTrackPeak: gg.P(4.0),
})
songAntennaWithLyrics = mf(model.MediaFile{
ID: "1005",

View file

@ -9,6 +9,7 @@ import (
"strings"
"github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -91,7 +92,7 @@ var _ = Describe("sendResponse", func() {
It("should return a fail response", func() {
payload.Song = &responses.Child{OpenSubsonicChild: &responses.OpenSubsonicChild{}}
// An +Inf value will cause an error when marshalling to JSON
payload.Song.ReplayGain = responses.ReplayGain{TrackGain: math.Inf(1)}
payload.Song.ReplayGain = responses.ReplayGain{TrackGain: gg.P(math.Inf(1))}
q := r.URL.Query()
q.Add("f", "json")
r.URL.RawQuery = q.Encode()

View file

@ -166,6 +166,52 @@
],
"displayComposer": "composer 1 \u0026 composer 2",
"explicitStatus": "clean"
},
{
"id": "2",
"isDir": true,
"title": "title",
"album": "album",
"artist": "artist",
"track": 1,
"year": 1985,
"genre": "Rock",
"coverArt": "1",
"size": 8421341,
"contentType": "audio/flac",
"suffix": "flac",
"starred": "2016-03-02T20:30:00Z",
"transcodedContentType": "audio/mpeg",
"transcodedSuffix": "mp3",
"duration": 146,
"bitRate": 320,
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"isrc": [],
"genres": [],
"replayGain": {
"trackGain": 0,
"albumGain": 0,
"trackPeak": 0,
"albumPeak": 0,
"baseGain": 0,
"fallbackGain": 0
},
"channelCount": 0,
"samplingRate": 0,
"bitDepth": 0,
"moods": [],
"artists": [],
"displayArtist": "",
"albumArtists": [],
"displayAlbumArtist": "",
"contributors": [],
"displayComposer": "",
"explicitStatus": ""
}
]
}

View file

@ -33,5 +33,8 @@
<artist id="2" name="artist2"></artist>
</contributors>
</song>
<song id="2" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false">
<replayGain trackGain="0" albumGain="0" trackPeak="0" albumPeak="0" baseGain="0" fallbackGain="0"></replayGain>
</song>
</album>
</subsonic-response>

View file

@ -112,6 +112,37 @@
],
"displayComposer": "composer 1 \u0026 composer 2",
"explicitStatus": "clean"
},
{
"id": "",
"isDir": false,
"isVideo": false,
"bpm": 0,
"comment": "",
"sortName": "",
"mediaType": "",
"musicBrainzId": "",
"isrc": [],
"genres": [],
"replayGain": {
"trackGain": 0,
"albumGain": 0,
"trackPeak": 0,
"albumPeak": 0,
"baseGain": 0,
"fallbackGain": 0
},
"channelCount": 0,
"samplingRate": 0,
"bitDepth": 0,
"moods": [],
"artists": [],
"displayArtist": "",
"albumArtists": [],
"displayAlbumArtist": "",
"contributors": [],
"displayComposer": "",
"explicitStatus": ""
}
],
"id": "1",

View file

@ -25,5 +25,8 @@
<artist id="4" name="composer2"></artist>
</contributors>
</child>
<child id="" isDir="false" isVideo="false">
<replayGain trackGain="0" albumGain="0" trackPeak="0" albumPeak="0" baseGain="0" fallbackGain="0"></replayGain>
</child>
</directory>
</subsonic-response>

View file

@ -546,16 +546,16 @@ type ItemGenre struct {
}
type ReplayGain struct {
TrackGain float64 `xml:"trackGain,omitempty,attr" json:"trackGain,omitempty"`
AlbumGain float64 `xml:"albumGain,omitempty,attr" json:"albumGain,omitempty"`
TrackPeak float64 `xml:"trackPeak,omitempty,attr" json:"trackPeak,omitempty"`
AlbumPeak float64 `xml:"albumPeak,omitempty,attr" json:"albumPeak,omitempty"`
BaseGain float64 `xml:"baseGain,omitempty,attr" json:"baseGain,omitempty"`
FallbackGain float64 `xml:"fallbackGain,omitempty,attr" json:"fallbackGain,omitempty"`
TrackGain *float64 `xml:"trackGain,omitempty,attr" json:"trackGain,omitempty"`
AlbumGain *float64 `xml:"albumGain,omitempty,attr" json:"albumGain,omitempty"`
TrackPeak *float64 `xml:"trackPeak,omitempty,attr" json:"trackPeak,omitempty"`
AlbumPeak *float64 `xml:"albumPeak,omitempty,attr" json:"albumPeak,omitempty"`
BaseGain *float64 `xml:"baseGain,omitempty,attr" json:"baseGain,omitempty"`
FallbackGain *float64 `xml:"fallbackGain,omitempty,attr" json:"fallbackGain,omitempty"`
}
func (r ReplayGain) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if r.TrackGain == 0 && r.AlbumGain == 0 && r.TrackPeak == 0 && r.AlbumPeak == 0 && r.BaseGain == 0 && r.FallbackGain == 0 {
if r.TrackGain == nil && r.AlbumGain == nil && r.TrackPeak == nil && r.AlbumPeak == nil && r.BaseGain == nil && r.FallbackGain == nil {
return nil
}
type replayGain ReplayGain

View file

@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/consts"
. "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -213,7 +214,7 @@ var _ = Describe("Responses", func() {
Context("with data", func() {
BeforeEach(func() {
response.Directory = &Directory{Id: "1", Name: "N"}
child := make([]Child, 1)
child := make([]Child, 2)
t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
child[0] = Child{
Id: "1", IsDir: true, Title: "title", Album: "album", Artist: "artist", Track: 1,
@ -227,7 +228,7 @@ var _ = Describe("Responses", func() {
Isrc: []string{"ISRC-1", "ISRC-2"},
BPM: 127, ChannelCount: 2, SamplingRate: 44100, BitDepth: 16,
Moods: []string{"happy", "sad"},
ReplayGain: ReplayGain{TrackGain: 1, AlbumGain: 2, TrackPeak: 3, AlbumPeak: 4, BaseGain: 5, FallbackGain: 6},
ReplayGain: ReplayGain{TrackGain: gg.P(1.0), AlbumGain: gg.P(2.0), TrackPeak: gg.P(3.0), AlbumPeak: gg.P(4.0), BaseGain: gg.P(5.0), FallbackGain: gg.P(6.0)},
DisplayArtist: "artist 1 & artist 2",
Artists: []ArtistID3Ref{
{Id: "1", Name: "artist1"},
@ -247,6 +248,9 @@ var _ = Describe("Responses", func() {
},
ExplicitStatus: "clean",
}
child[1].OpenSubsonicChild = &OpenSubsonicChild{
ReplayGain: ReplayGain{TrackGain: gg.P(0.0), AlbumGain: gg.P(0.0), TrackPeak: gg.P(0.0), AlbumPeak: gg.P(0.0), BaseGain: gg.P(0.0), FallbackGain: gg.P(0.0)},
}
response.Directory.Child = child
})
@ -309,13 +313,18 @@ var _ = Describe("Responses", func() {
Year: 1985, Genre: "Rock", CoverArt: "1", Size: 8421341, ContentType: "audio/flac",
Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3",
Duration: 146, BitRate: 320, Starred: &t,
}, {
Id: "2", IsDir: true, Title: "title", Album: "album", Artist: "artist", Track: 1,
Year: 1985, Genre: "Rock", CoverArt: "1", Size: 8421341, ContentType: "audio/flac",
Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3",
Duration: 146, BitRate: 320, Starred: &t,
}}
songs[0].OpenSubsonicChild = &OpenSubsonicChild{
Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
Comment: "a comment", MediaType: MediaTypeSong, MusicBrainzId: "4321", SortName: "sorted song",
Isrc: []string{"ISRC-1"},
Moods: []string{"happy", "sad"},
ReplayGain: ReplayGain{TrackGain: 1, AlbumGain: 2, TrackPeak: 3, AlbumPeak: 4, BaseGain: 5, FallbackGain: 6},
ReplayGain: ReplayGain{TrackGain: gg.P(1.0), AlbumGain: gg.P(2.0), TrackPeak: gg.P(3.0), AlbumPeak: gg.P(4.0), BaseGain: gg.P(5.0), FallbackGain: gg.P(6.0)},
BPM: 127, ChannelCount: 2, SamplingRate: 44100, BitDepth: 16,
DisplayArtist: "artist1 & artist2",
Artists: []ArtistID3Ref{
@ -334,6 +343,9 @@ var _ = Describe("Responses", func() {
DisplayComposer: "composer 1 & composer 2",
ExplicitStatus: "clean",
}
songs[1].OpenSubsonicChild = &OpenSubsonicChild{
ReplayGain: ReplayGain{TrackGain: gg.P(0.0), AlbumGain: gg.P(0.0), TrackPeak: gg.P(0.0), AlbumPeak: gg.P(0.0), BaseGain: gg.P(0.0), FallbackGain: gg.P(0.0)},
}
response.AlbumWithSongsID3.AlbumID3 = album
response.AlbumWithSongsID3.Song = songs
})

BIN
tests/fixtures/no_replaygain.mp3 vendored Normal file

Binary file not shown.

BIN
tests/fixtures/zero_replaygain.mp3 vendored Normal file

Binary file not shown.