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
17 changed files with 279 additions and 96 deletions
+24
View File
@@ -8,6 +8,7 @@ import (
"github.com/djherbis/times" "github.com/djherbis/times"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/metadata" "github.com/navidrome/navidrome/model/metadata"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@@ -82,6 +83,29 @@ var _ = Describe("Extractor", func() {
e = &extractor{} 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() { Describe("Participants", func() {
DescribeTable("test tags consistent across formats", func(format string) { DescribeTable("test tags consistent across formats", func(format string) {
path := "tests/fixtures/test." + format path := "tests/fixtures/test." + format
@@ -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
}
+47 -47
View File
@@ -36,53 +36,53 @@ type MediaFile struct {
Artist string `structs:"artist" json:"artist"` Artist string `structs:"artist" json:"artist"`
AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"` // Deprecated: Use Participants instead AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"` // Deprecated: Use Participants instead
// AlbumArtist is the display name used for the album artist. // AlbumArtist is the display name used for the album artist.
AlbumArtist string `structs:"album_artist" json:"albumArtist"` AlbumArtist string `structs:"album_artist" json:"albumArtist"`
AlbumID string `structs:"album_id" json:"albumId"` AlbumID string `structs:"album_id" json:"albumId"`
HasCoverArt bool `structs:"has_cover_art" json:"hasCoverArt"` HasCoverArt bool `structs:"has_cover_art" json:"hasCoverArt"`
TrackNumber int `structs:"track_number" json:"trackNumber"` TrackNumber int `structs:"track_number" json:"trackNumber"`
DiscNumber int `structs:"disc_number" json:"discNumber"` DiscNumber int `structs:"disc_number" json:"discNumber"`
DiscSubtitle string `structs:"disc_subtitle" json:"discSubtitle,omitempty"` DiscSubtitle string `structs:"disc_subtitle" json:"discSubtitle,omitempty"`
Year int `structs:"year" json:"year"` Year int `structs:"year" json:"year"`
Date string `structs:"date" json:"date,omitempty"` Date string `structs:"date" json:"date,omitempty"`
OriginalYear int `structs:"original_year" json:"originalYear"` OriginalYear int `structs:"original_year" json:"originalYear"`
OriginalDate string `structs:"original_date" json:"originalDate,omitempty"` OriginalDate string `structs:"original_date" json:"originalDate,omitempty"`
ReleaseYear int `structs:"release_year" json:"releaseYear"` ReleaseYear int `structs:"release_year" json:"releaseYear"`
ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"` ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"`
Size int64 `structs:"size" json:"size"` Size int64 `structs:"size" json:"size"`
Suffix string `structs:"suffix" json:"suffix"` Suffix string `structs:"suffix" json:"suffix"`
Duration float32 `structs:"duration" json:"duration"` Duration float32 `structs:"duration" json:"duration"`
BitRate int `structs:"bit_rate" json:"bitRate"` BitRate int `structs:"bit_rate" json:"bitRate"`
SampleRate int `structs:"sample_rate" json:"sampleRate"` SampleRate int `structs:"sample_rate" json:"sampleRate"`
BitDepth int `structs:"bit_depth" json:"bitDepth"` BitDepth int `structs:"bit_depth" json:"bitDepth"`
Channels int `structs:"channels" json:"channels"` Channels int `structs:"channels" json:"channels"`
Genre string `structs:"genre" json:"genre"` Genre string `structs:"genre" json:"genre"`
Genres Genres `structs:"-" json:"genres,omitempty"` Genres Genres `structs:"-" json:"genres,omitempty"`
SortTitle string `structs:"sort_title" json:"sortTitle,omitempty"` SortTitle string `structs:"sort_title" json:"sortTitle,omitempty"`
SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"` SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"`
SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"` // Deprecated: Use Participants instead 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 SortAlbumArtistName string `structs:"sort_album_artist_name" json:"sortAlbumArtistName,omitempty"` // Deprecated: Use Participants instead
OrderTitle string `structs:"order_title" json:"orderTitle,omitempty"` OrderTitle string `structs:"order_title" json:"orderTitle,omitempty"`
OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"` OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"`
OrderArtistName string `structs:"order_artist_name" json:"orderArtistName"` // Deprecated: Use Participants instead 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 OrderAlbumArtistName string `structs:"order_album_artist_name" json:"orderAlbumArtistName"` // Deprecated: Use Participants instead
Compilation bool `structs:"compilation" json:"compilation"` Compilation bool `structs:"compilation" json:"compilation"`
Comment string `structs:"comment" json:"comment,omitempty"` Comment string `structs:"comment" json:"comment,omitempty"`
Lyrics string `structs:"lyrics" json:"lyrics"` Lyrics string `structs:"lyrics" json:"lyrics"`
BPM int `structs:"bpm" json:"bpm,omitempty"` BPM int `structs:"bpm" json:"bpm,omitempty"`
ExplicitStatus string `structs:"explicit_status" json:"explicitStatus"` ExplicitStatus string `structs:"explicit_status" json:"explicitStatus"`
CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"` CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"`
MbzRecordingID string `structs:"mbz_recording_id" json:"mbzRecordingID,omitempty"` MbzRecordingID string `structs:"mbz_recording_id" json:"mbzRecordingID,omitempty"`
MbzReleaseTrackID string `structs:"mbz_release_track_id" json:"mbzReleaseTrackId,omitempty"` MbzReleaseTrackID string `structs:"mbz_release_track_id" json:"mbzReleaseTrackId,omitempty"`
MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty"` MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty"`
MbzReleaseGroupID string `structs:"mbz_release_group_id" json:"mbzReleaseGroupId,omitempty"` MbzReleaseGroupID string `structs:"mbz_release_group_id" json:"mbzReleaseGroupId,omitempty"`
MbzArtistID string `structs:"mbz_artist_id" json:"mbzArtistId,omitempty"` // Deprecated: Use Participants instead 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 MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty"` // Deprecated: Use Participants instead
MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"` MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"`
MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"` MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"`
RGAlbumGain float64 `structs:"rg_album_gain" json:"rgAlbumGain"` RGAlbumGain *float64 `structs:"rg_album_gain" json:"rgAlbumGain"`
RGAlbumPeak float64 `structs:"rg_album_peak" json:"rgAlbumPeak"` RGAlbumPeak *float64 `structs:"rg_album_peak" json:"rgAlbumPeak"`
RGTrackGain float64 `structs:"rg_track_gain" json:"rgTrackGain"` RGTrackGain *float64 `structs:"rg_track_gain" json:"rgTrackGain"`
RGTrackPeak float64 `structs:"rg_track_peak" json:"rgTrackPeak"` RGTrackPeak *float64 `structs:"rg_track_peak" json:"rgTrackPeak"`
Tags Tags `structs:"tags" json:"tags,omitempty" hash:"ignore"` // All imported tags from the original file 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 Participants Participants `structs:"participants" json:"participants" hash:"ignore"` // All artists that participated in this track
+9 -8
View File
@@ -53,9 +53,9 @@ func (md Metadata) ToMediaFile(libID int, folderID string) model.MediaFile {
mf.MbzAlbumType = md.String(model.TagReleaseType) mf.MbzAlbumType = md.String(model.TagReleaseType)
// ReplayGain // ReplayGain
mf.RGAlbumPeak = md.Float(model.TagReplayGainAlbumPeak, 1) mf.RGAlbumPeak = md.NullableFloat(model.TagReplayGainAlbumPeak)
mf.RGAlbumGain = md.mapGain(model.TagReplayGainAlbumGain, model.TagR128AlbumGain) 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) mf.RGTrackGain = md.mapGain(model.TagReplayGainTrackGain, model.TagR128TrackGain)
// General properties // General properties
@@ -108,23 +108,24 @@ func (md Metadata) AlbumID(mf model.MediaFile, pidConf string) string {
return getPID(mf, md, pidConf) 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) v := md.Gain(rg)
if v != 0 { if v != nil {
return v return v
} }
r128value := md.String(r128) r128value := md.String(r128)
if r128value != "" { if r128value != "" {
var v, err = strconv.Atoi(r128value) var v, err = strconv.Atoi(r128value)
if err != nil { if err != nil {
return 0 return nil
} }
// Convert Q7.8 to float // Convert Q7.8 to float
var value = float64(v) / 256.0 value := float64(v) / 256.0
// Adding 5 dB to normalize with ReplayGain level // Adding 5 dB to normalize with ReplayGain level
return value + 5 value += 5
return &value
} }
return 0 return nil
} }
func (md Metadata) mapLyrics() string { func (md Metadata) mapLyrics() string {
+17 -7
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 { func (md Metadata) Float(key model.TagName, def ...float64) float64 {
return float(md.first(key), def...) 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)) v := strings.TrimSpace(strings.Replace(md.first(key), "dB", "", 1))
return float(v) return nullableFloat(v)
} }
func (md Metadata) Pairs(key model.TagName) []Pair { func (md Metadata) Pairs(key model.TagName) []Pair {
values := md.tags[key] values := md.tags[key]
@@ -119,14 +121,22 @@ func (md Metadata) first(key model.TagName) string {
} }
func float(value string, def ...float64) float64 { 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) v, err := strconv.ParseFloat(value, 64)
if err != nil || v == math.Inf(-1) || math.IsInf(v, 1) || math.IsNaN(v) { if err != nil || v == math.Inf(-1) || math.IsInf(v, 1) || math.IsNaN(v) {
if len(def) > 0 { return nil
return def[0]
}
return 0
} }
return v return &v
} }
// Used for tracks and discs // Used for tracks and discs
+20 -18
View File
@@ -8,6 +8,7 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/metadata" "github.com/navidrome/navidrome/model/metadata"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@@ -257,38 +258,39 @@ var _ = Describe("Metadata", func() {
} }
DescribeTable("Gain", DescribeTable("Gain",
func(tagValue string, expected float64) { func(tagValue string, expected *float64) {
mf := createMF("replaygain_track_gain", tagValue) mf := createMF("replaygain_track_gain", tagValue)
Expect(mf.RGTrackGain).To(Equal(expected)) Expect(mf.RGTrackGain).To(Equal(expected))
}, },
Entry("0", "0", 0.0), Entry("0", "0", gg.P(0.0)),
Entry("1.2dB", "1.2dB", 1.2), Entry("1.2dB", "1.2dB", gg.P(1.2)),
Entry("Infinity", "Infinity", 0.0), Entry("Infinity", "Infinity", nil),
Entry("Invalid value", "INVALID VALUE", 0.0), Entry("Invalid value", "INVALID VALUE", nil),
Entry("NaN", "NaN", 0.0), Entry("NaN", "NaN", nil),
) )
DescribeTable("Peak", DescribeTable("Peak",
func(tagValue string, expected float64) { func(tagValue string, expected *float64) {
mf := createMF("replaygain_track_peak", tagValue) mf := createMF("replaygain_track_peak", tagValue)
Expect(mf.RGTrackPeak).To(Equal(expected)) Expect(mf.RGTrackPeak).To(Equal(expected))
}, },
Entry("0", "0", 0.0), Entry("0", "0", gg.P(0.0)),
Entry("0.5", "0.5", 0.5), Entry("1.0", "1.0", gg.P(1.0)),
Entry("Invalid dB suffix", "0.7dB", 1.0), Entry("0.5", "0.5", gg.P(0.5)),
Entry("Infinity", "Infinity", 1.0), Entry("Invalid dB suffix", "0.7dB", nil),
Entry("Invalid value", "INVALID VALUE", 1.0), Entry("Infinity", "Infinity", nil),
Entry("NaN", "NaN", 1.0), Entry("Invalid value", "INVALID VALUE", nil),
Entry("NaN", "NaN", nil),
) )
DescribeTable("getR128GainValue", DescribeTable("getR128GainValue",
func(tagValue string, expected float64) { func(tagValue string, expected *float64) {
mf := createMF("r128_track_gain", tagValue) mf := createMF("r128_track_gain", tagValue)
Expect(mf.RGTrackGain).To(Equal(expected)) Expect(mf.RGTrackGain).To(Equal(expected))
}, },
Entry("0", "0", 5.0), Entry("0", "0", gg.P(5.0)),
Entry("-3776", "-3776", -9.75), Entry("-3776", "-3776", gg.P(-9.75)),
Entry("Infinity", "Infinity", 0.0), Entry("Infinity", "Infinity", nil),
Entry("Invalid value", "INVALID VALUE", 0.0), Entry("Invalid value", "INVALID VALUE", nil),
) )
}) })
+4 -4
View File
@@ -25,10 +25,10 @@ type dbMediaFile struct {
Tags string `structs:"-" json:"-"` Tags string `structs:"-" json:"-"`
// These are necessary to map the correct names (rg_*) to the correct fields (RG*) // These are necessary to map the correct names (rg_*) to the correct fields (RG*)
// without using `db` struct tags in the model.MediaFile struct // without using `db` struct tags in the model.MediaFile struct
RgAlbumGain float64 `structs:"-" json:"-"` RgAlbumGain *float64 `structs:"-" json:"-"`
RgAlbumPeak float64 `structs:"-" json:"-"` RgAlbumPeak *float64 `structs:"-" json:"-"`
RgTrackGain float64 `structs:"-" json:"-"` RgTrackGain *float64 `structs:"-" json:"-"`
RgTrackPeak float64 `structs:"-" json:"-"` RgTrackPeak *float64 `structs:"-" json:"-"`
} }
func (m *dbMediaFile) PostScan() error { func (m *dbMediaFile) PostScan() error {
+2 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/tests" "github.com/navidrome/navidrome/tests"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/pocketbase/dbx" "github.com/pocketbase/dbx"
@@ -79,7 +80,7 @@ var (
songAntenna = mf(model.MediaFile{ID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", songAntenna = mf(model.MediaFile{ID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk",
AlbumID: "103", AlbumID: "103",
Path: p("/kraft/radio/antenna.mp3"), 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{ songAntennaWithLyrics = mf(model.MediaFile{
ID: "1005", ID: "1005",
+2 -1
View File
@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@@ -91,7 +92,7 @@ var _ = Describe("sendResponse", func() {
It("should return a fail response", func() { It("should return a fail response", func() {
payload.Song = &responses.Child{OpenSubsonicChild: &responses.OpenSubsonicChild{}} payload.Song = &responses.Child{OpenSubsonicChild: &responses.OpenSubsonicChild{}}
// An +Inf value will cause an error when marshalling to JSON // 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 := r.URL.Query()
q.Add("f", "json") q.Add("f", "json")
r.URL.RawQuery = q.Encode() r.URL.RawQuery = q.Encode()
@@ -166,6 +166,52 @@
], ],
"displayComposer": "composer 1 \u0026 composer 2", "displayComposer": "composer 1 \u0026 composer 2",
"explicitStatus": "clean" "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": ""
} }
] ]
} }
@@ -33,5 +33,8 @@
<artist id="2" name="artist2"></artist> <artist id="2" name="artist2"></artist>
</contributors> </contributors>
</song> </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> </album>
</subsonic-response> </subsonic-response>
@@ -112,6 +112,37 @@
], ],
"displayComposer": "composer 1 \u0026 composer 2", "displayComposer": "composer 1 \u0026 composer 2",
"explicitStatus": "clean" "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", "id": "1",
@@ -25,5 +25,8 @@
<artist id="4" name="composer2"></artist> <artist id="4" name="composer2"></artist>
</contributors> </contributors>
</child> </child>
<child id="" isDir="false" isVideo="false">
<replayGain trackGain="0" albumGain="0" trackPeak="0" albumPeak="0" baseGain="0" fallbackGain="0"></replayGain>
</child>
</directory> </directory>
</subsonic-response> </subsonic-response>
+7 -7
View File
@@ -546,16 +546,16 @@ type ItemGenre struct {
} }
type ReplayGain struct { type ReplayGain struct {
TrackGain float64 `xml:"trackGain,omitempty,attr" json:"trackGain,omitempty"` TrackGain *float64 `xml:"trackGain,omitempty,attr" json:"trackGain,omitempty"`
AlbumGain float64 `xml:"albumGain,omitempty,attr" json:"albumGain,omitempty"` AlbumGain *float64 `xml:"albumGain,omitempty,attr" json:"albumGain,omitempty"`
TrackPeak float64 `xml:"trackPeak,omitempty,attr" json:"trackPeak,omitempty"` TrackPeak *float64 `xml:"trackPeak,omitempty,attr" json:"trackPeak,omitempty"`
AlbumPeak float64 `xml:"albumPeak,omitempty,attr" json:"albumPeak,omitempty"` AlbumPeak *float64 `xml:"albumPeak,omitempty,attr" json:"albumPeak,omitempty"`
BaseGain float64 `xml:"baseGain,omitempty,attr" json:"baseGain,omitempty"` BaseGain *float64 `xml:"baseGain,omitempty,attr" json:"baseGain,omitempty"`
FallbackGain float64 `xml:"fallbackGain,omitempty,attr" json:"fallbackGain,omitempty"` FallbackGain *float64 `xml:"fallbackGain,omitempty,attr" json:"fallbackGain,omitempty"`
} }
func (r ReplayGain) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 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 return nil
} }
type replayGain ReplayGain type replayGain ReplayGain
+15 -3
View File
@@ -12,6 +12,7 @@ import (
"github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/consts"
. "github.com/navidrome/navidrome/server/subsonic/responses" . "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@@ -213,7 +214,7 @@ var _ = Describe("Responses", func() {
Context("with data", func() { Context("with data", func() {
BeforeEach(func() { BeforeEach(func() {
response.Directory = &Directory{Id: "1", Name: "N"} 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) t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
child[0] = Child{ child[0] = Child{
Id: "1", IsDir: true, Title: "title", Album: "album", Artist: "artist", Track: 1, 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"}, Isrc: []string{"ISRC-1", "ISRC-2"},
BPM: 127, ChannelCount: 2, SamplingRate: 44100, BitDepth: 16, BPM: 127, ChannelCount: 2, SamplingRate: 44100, BitDepth: 16,
Moods: []string{"happy", "sad"}, 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", DisplayArtist: "artist 1 & artist 2",
Artists: []ArtistID3Ref{ Artists: []ArtistID3Ref{
{Id: "1", Name: "artist1"}, {Id: "1", Name: "artist1"},
@@ -247,6 +248,9 @@ var _ = Describe("Responses", func() {
}, },
ExplicitStatus: "clean", 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 response.Directory.Child = child
}) })
@@ -309,13 +313,18 @@ var _ = Describe("Responses", func() {
Year: 1985, Genre: "Rock", CoverArt: "1", Size: 8421341, ContentType: "audio/flac", Year: 1985, Genre: "Rock", CoverArt: "1", Size: 8421341, ContentType: "audio/flac",
Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3", Suffix: "flac", TranscodedContentType: "audio/mpeg", TranscodedSuffix: "mp3",
Duration: 146, BitRate: 320, Starred: &t, 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{ songs[0].OpenSubsonicChild = &OpenSubsonicChild{
Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}}, Genres: []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
Comment: "a comment", MediaType: MediaTypeSong, MusicBrainzId: "4321", SortName: "sorted song", Comment: "a comment", MediaType: MediaTypeSong, MusicBrainzId: "4321", SortName: "sorted song",
Isrc: []string{"ISRC-1"}, Isrc: []string{"ISRC-1"},
Moods: []string{"happy", "sad"}, 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, BPM: 127, ChannelCount: 2, SamplingRate: 44100, BitDepth: 16,
DisplayArtist: "artist1 & artist2", DisplayArtist: "artist1 & artist2",
Artists: []ArtistID3Ref{ Artists: []ArtistID3Ref{
@@ -334,6 +343,9 @@ var _ = Describe("Responses", func() {
DisplayComposer: "composer 1 & composer 2", DisplayComposer: "composer 1 & composer 2",
ExplicitStatus: "clean", 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.AlbumID3 = album
response.AlbumWithSongsID3.Song = songs response.AlbumWithSongsID3.Song = songs
}) })
Binary file not shown.
Binary file not shown.