package library import ( "context" "errors" "fmt" "sync" "time" "github.com/jackc/pgx/v5/pgxpool" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" ) // Loudness analysis settings (M464 #4995). Rule 25: an operator setting is a // database row, changed without a restart. One instance is shared by the // backfill and the admin API, so a save reaches the worker at once. // LoudnessSettings mirrors the loudness_settings row. type LoudnessSettings struct { // Enabled off stops the background analysis. Measured tracks keep their // values, so normalization keeps working for them. Enabled bool BackfillConcurrency int32 // UpdatedAt is set by the database; ignored by Set. UpdatedAt time.Time } // DefaultLoudnessSettings mirrors migration 0065's column defaults, so a // database that cannot be read still analyzes the way a fresh install does. var DefaultLoudnessSettings = LoudnessSettings{ Enabled: true, BackfillConcurrency: loudnessBackfillConcurrency, } // ErrLoudnessSettingOutOfRange is returned by Set for a value migration 0065's // CHECK would reject, so the API answers 400 naming the field. var ErrLoudnessSettingOutOfRange = errors.New("loudness setting out of range") // LoudnessSettingsService caches the settings and owns their persistence. type LoudnessSettingsService struct { pool *pgxpool.Pool mu sync.RWMutex cur LoudnessSettings } // NewLoudnessSettingsService loads once and caches. It always returns a usable // service, holding the defaults when the load fails; the error says so. func NewLoudnessSettingsService(ctx context.Context, pool *pgxpool.Pool) (*LoudnessSettingsService, error) { s := &LoudnessSettingsService{pool: pool, cur: DefaultLoudnessSettings} row, err := dbq.New(pool).GetLoudnessSettings(ctx) if err != nil { return s, fmt.Errorf("loudness settings: load: %w", err) } s.cur = loudnessSettingsFromRow(row) return s, nil } // Get returns the cached settings. A nil service answers with the defaults. func (s *LoudnessSettingsService) Get() LoudnessSettings { if s == nil { return DefaultLoudnessSettings } s.mu.RLock() defer s.mu.RUnlock() return s.cur } // Set validates, persists and re-caches. func (s *LoudnessSettingsService) Set(ctx context.Context, in LoudnessSettings) (LoudnessSettings, error) { if err := validateLoudnessSettings(in); err != nil { return LoudnessSettings{}, err } if s == nil { return LoudnessSettings{}, errors.New("loudness settings: no settings service") } row, err := dbq.New(s.pool).UpdateLoudnessSettings(ctx, dbq.UpdateLoudnessSettingsParams{ Enabled: in.Enabled, BackfillConcurrency: in.BackfillConcurrency, }) if err != nil { return LoudnessSettings{}, fmt.Errorf("loudness settings: save: %w", err) } out := loudnessSettingsFromRow(row) s.mu.Lock() s.cur = out s.mu.Unlock() return out, nil } func validateLoudnessSettings(in LoudnessSettings) error { if in.BackfillConcurrency < minBackfillConcurrency || in.BackfillConcurrency > maxBackfillConcurrency { return fmt.Errorf("%w: backfill_concurrency must be %d-%d", ErrLoudnessSettingOutOfRange, minBackfillConcurrency, maxBackfillConcurrency) } return nil } func loudnessSettingsFromRow(row dbq.LoudnessSetting) LoudnessSettings { return LoudnessSettings{ Enabled: row.Enabled, BackfillConcurrency: row.BackfillConcurrency, UpdatedAt: row.UpdatedAt.Time, } }