mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-03 06:41:01 +00:00
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:
parent
4359adc042
commit
7640c474cf
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
BIN
tests/fixtures/no_replaygain.mp3
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/zero_replaygain.mp3
vendored
Normal file
BIN
tests/fixtures/zero_replaygain.mp3
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user