package library import ( "context" "errors" "fmt" "slices" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // NormalizationPrefs is one user's loudness-normalization preference (M464 // #4998). It lives on the server so every player applies the same one: the // web player and the Android app read it, and the leveled stream for // UPnP/Sonos (#5001) renders with it. type NormalizationPrefs struct { // Mode is off, auto, track or album. Auto uses album gain while an album // plays in order and track gain otherwise. Mode string `json:"mode"` // TargetLUFS is the loudness tracks are leveled to. TargetLUFS int16 `json:"target_lufs"` // Boost is how a track quieter than the target is raised: headroom stops // at its true peak, limiter goes all the way and limits the peaks. Boost string `json:"boost"` } // The accepted values. Migration 0067's CHECKs hold the same lists. var ( NormalizationModes = []string{"off", "auto", "track", "album"} NormalizationTargets = []int16{-18, -16, -14} NormalizationBoosts = []string{"headroom", "limiter"} ) // DefaultNormalizationPrefs applies to a user who has never chosen. It must // equal migration 0067's column defaults; a test holds them together. var DefaultNormalizationPrefs = NormalizationPrefs{Mode: "auto", TargetLUFS: -18, Boost: "headroom"} // ErrNormalizationPrefsInvalid is returned for a value outside the lists. var ErrNormalizationPrefsInvalid = errors.New("normalization preference out of range") // Validate reports the first field holding a value the server does not // accept. func (p NormalizationPrefs) Validate() error { switch { case !slices.Contains(NormalizationModes, p.Mode): return fmt.Errorf("%w: mode %q", ErrNormalizationPrefsInvalid, p.Mode) case !slices.Contains(NormalizationTargets, p.TargetLUFS): return fmt.Errorf("%w: target_lufs %d", ErrNormalizationPrefsInvalid, p.TargetLUFS) case !slices.Contains(NormalizationBoosts, p.Boost): return fmt.Errorf("%w: boost %q", ErrNormalizationPrefsInvalid, p.Boost) } return nil } // LoadNormalizationPrefs returns the user's preference, or the defaults if // they have never set one. func LoadNormalizationPrefs(ctx context.Context, q *dbq.Queries, userID pgtype.UUID) (NormalizationPrefs, error) { row, err := q.GetNormalizationPrefs(ctx, userID) if errors.Is(err, pgx.ErrNoRows) { return DefaultNormalizationPrefs, nil } if err != nil { return DefaultNormalizationPrefs, err } return NormalizationPrefs{Mode: row.Mode, TargetLUFS: row.TargetLufs, Boost: row.Boost}, nil } // SaveNormalizationPrefs validates and stores the user's preference, // returning what was stored. func SaveNormalizationPrefs(ctx context.Context, q *dbq.Queries, userID pgtype.UUID, p NormalizationPrefs) (NormalizationPrefs, error) { if err := p.Validate(); err != nil { return p, err } row, err := q.UpsertNormalizationPrefs(ctx, dbq.UpsertNormalizationPrefsParams{ UserID: userID, Mode: p.Mode, TargetLufs: p.TargetLUFS, Boost: p.Boost, }) if err != nil { return p, err } return NormalizationPrefs{Mode: row.Mode, TargetLUFS: row.TargetLufs, Boost: row.Boost}, nil }