Files
minstrel/internal/library/normalization_prefs.go
T
bvandeusenandClaude Opus 5.5 7e6228c13f
release / govulncheck (push) Successful in 20s
release / web (push) Successful in 1m20s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m15s
release / android (push) Successful in 5m26s
release / Build signed APK (releases and dev) (push) Successful in 5m45s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
docs: library comments say what the scanner and resolver do, not how they came to be (#6277)
Rewrites the internal/library comment blocks to rule 32. Task, milestone,
decision and rule ids are gone; the incidents behind them are restated as
the constraint they left (why a file is deleted before its row, why the
missing mark is a mark, why duplicates block on duration). Counts and
album names from one library are dropped from production comments; test
comments keep the names their fixtures use. The tagReadVersion log keeps
each version's before and after, without the measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:27:34 -04:00

84 lines
3.1 KiB
Go

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. 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 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
}