diff --git a/adapters/taglib/end_to_end_test.go b/adapters/taglib/end_to_end_test.go
index 08fc1a50..0b512654 100644
--- a/adapters/taglib/end_to_end_test.go
+++ b/adapters/taglib/end_to_end_test.go
@@ -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
diff --git a/db/migrations/20250701010104_make_replaygain_fields_nullable.go b/db/migrations/20250701010104_make_replaygain_fields_nullable.go
new file mode 100644
index 00000000..163608d3
--- /dev/null
+++ b/db/migrations/20250701010104_make_replaygain_fields_nullable.go
@@ -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
+}
diff --git a/model/mediafile.go b/model/mediafile.go
index 5068e5d0..d29a2a50 100644
--- a/model/mediafile.go
+++ b/model/mediafile.go
@@ -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
diff --git a/model/metadata/map_mediafile.go b/model/metadata/map_mediafile.go
index b4857df8..591b618a 100644
--- a/model/metadata/map_mediafile.go
+++ b/model/metadata/map_mediafile.go
@@ -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 {
diff --git a/model/metadata/metadata.go b/model/metadata/metadata.go
index 471c2434..aea4238a 100644
--- a/model/metadata/metadata.go
+++ b/model/metadata/metadata.go
@@ -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
diff --git a/model/metadata/metadata_test.go b/model/metadata/metadata_test.go
index d7473afa..82afd865 100644
--- a/model/metadata/metadata_test.go
+++ b/model/metadata/metadata_test.go
@@ -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),
)
})
diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go
index b0ed637c..eee6444c 100644
--- a/persistence/mediafile_repository.go
+++ b/persistence/mediafile_repository.go
@@ -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 {
diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go
index fc451913..7edfeee1 100644
--- a/persistence/persistence_suite_test.go
+++ b/persistence/persistence_suite_test.go
@@ -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",
diff --git a/server/subsonic/api_test.go b/server/subsonic/api_test.go
index 5d248c46..1658f094 100644
--- a/server/subsonic/api_test.go
+++ b/server/subsonic/api_test.go
@@ -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()
diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON
index 78b5c6e7..c2a29b22 100644
--- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON
+++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON
@@ -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": ""
}
]
}
diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML
index f3281d9e..1ad3e600 100644
--- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML
+++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML
@@ -33,5 +33,8 @@
+
+
+
diff --git a/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON
index d64ae9e7..fde40646 100644
--- a/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON
+++ b/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON
@@ -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",
diff --git a/server/subsonic/responses/.snapshots/Responses Child with data should match .XML b/server/subsonic/responses/.snapshots/Responses Child with data should match .XML
index 639fd3f6..faea8ee9 100644
--- a/server/subsonic/responses/.snapshots/Responses Child with data should match .XML
+++ b/server/subsonic/responses/.snapshots/Responses Child with data should match .XML
@@ -25,5 +25,8 @@
+
+
+
diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go
index 4a7ebbe8..ffda2aa4 100644
--- a/server/subsonic/responses/responses.go
+++ b/server/subsonic/responses/responses.go
@@ -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
diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go
index 9fcd6078..7238665c 100644
--- a/server/subsonic/responses/responses_test.go
+++ b/server/subsonic/responses/responses_test.go
@@ -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
})
diff --git a/tests/fixtures/no_replaygain.mp3 b/tests/fixtures/no_replaygain.mp3
new file mode 100644
index 00000000..45c2176e
Binary files /dev/null and b/tests/fixtures/no_replaygain.mp3 differ
diff --git a/tests/fixtures/zero_replaygain.mp3 b/tests/fixtures/zero_replaygain.mp3
new file mode 100644
index 00000000..96e6d21f
Binary files /dev/null and b/tests/fixtures/zero_replaygain.mp3 differ