add: DB-backed settings UI for live-editable tunables
Add a settings page (/settings) and API (/api/settings GET/PUT) to edit encoding profiles (crf/preset per media type), lp, OMDb key, log retention, and delete-originals from the browser, persisted to a settings table in logs.db. config.yaml seeds the store on first run; afterwards the DB is the source of truth (paths.* and http_addr stay config-only). Each job snapshots the current settings, so changes apply to the next encode with no restart. PUT validates ranges (crf 0-63, preset 0-13, lp >=0, retention >=1) before persisting. No auth, by design for now (isolated network) — see #2. Closes #1
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// Package settings holds the live-editable configuration: values that can be
|
||||
// changed from the web UI and persisted, without a config-file edit + restart.
|
||||
// config.yaml seeds the store on first run; afterwards the DB is the source of
|
||||
// truth for these values (paths.* and http_addr stay config-only).
|
||||
package settings
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"av1dae/pkg/types"
|
||||
)
|
||||
|
||||
// Profile is a per-source-type SVT-AV1 quality pair.
|
||||
type Profile struct {
|
||||
CRF int `json:"crf"`
|
||||
Preset int `json:"preset"`
|
||||
}
|
||||
|
||||
// Settings is the full set of live-editable values.
|
||||
type Settings struct {
|
||||
DVD Profile `json:"dvd"`
|
||||
Bluray Profile `json:"bluray"`
|
||||
WebDL Profile `json:"webdl"`
|
||||
TVRip Profile `json:"tvrip"`
|
||||
LP int `json:"lp"`
|
||||
OMDBAPIKey string `json:"omdb_api_key"`
|
||||
LogRetentionDays int `json:"log_retention_days"`
|
||||
DeleteOriginals bool `json:"delete_originals"`
|
||||
}
|
||||
|
||||
// ProfileFor returns the encoding profile for a media type.
|
||||
func (s Settings) ProfileFor(mt types.MediaType) Profile {
|
||||
switch mt {
|
||||
case types.MediaTypeBluRay:
|
||||
return s.Bluray
|
||||
case types.MediaTypeWebDL:
|
||||
return s.WebDL
|
||||
case types.MediaTypeTVRip:
|
||||
return s.TVRip
|
||||
default:
|
||||
return s.DVD
|
||||
}
|
||||
}
|
||||
|
||||
// Validate guards the trust boundary: a bad value from the PUT handler could
|
||||
// silently break every subsequent encode. Ranges follow SVT-AV1's limits.
|
||||
func (s Settings) Validate() error {
|
||||
for name, p := range map[string]Profile{"dvd": s.DVD, "bluray": s.Bluray, "webdl": s.WebDL, "tvrip": s.TVRip} {
|
||||
if p.CRF < 0 || p.CRF > 63 {
|
||||
return fmt.Errorf("%s crf %d out of range 0-63", name, p.CRF)
|
||||
}
|
||||
if p.Preset < 0 || p.Preset > 13 {
|
||||
return fmt.Errorf("%s preset %d out of range 0-13", name, p.Preset)
|
||||
}
|
||||
}
|
||||
if s.LP < 0 {
|
||||
return fmt.Errorf("lp %d must be >= 0", s.LP)
|
||||
}
|
||||
if s.LogRetentionDays < 1 {
|
||||
return fmt.Errorf("log_retention_days %d must be >= 1", s.LogRetentionDays)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store is the persisted, concurrency-safe settings holder. The encode loop
|
||||
// reads via Get; the HTTP handler writes via Set.
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
db *sql.DB
|
||||
cur Settings
|
||||
}
|
||||
|
||||
// New creates the settings table if needed, loads the persisted row, or seeds
|
||||
// it from `seed` (the config-derived values) on first run.
|
||||
func New(db *sql.DB, seed Settings) (*Store, error) {
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL)`); err != nil {
|
||||
return nil, fmt.Errorf("settings schema: %w", err)
|
||||
}
|
||||
s := &Store{db: db, cur: seed}
|
||||
|
||||
var data string
|
||||
switch err := db.QueryRow(`SELECT data FROM settings WHERE id = 1`).Scan(&data); err {
|
||||
case sql.ErrNoRows:
|
||||
if err := s.persist(seed); err != nil { // first run — seed from config
|
||||
return nil, err
|
||||
}
|
||||
case nil:
|
||||
var loaded Settings
|
||||
if err := json.Unmarshal([]byte(data), &loaded); err != nil {
|
||||
return nil, fmt.Errorf("decoding settings: %w", err)
|
||||
}
|
||||
s.cur = loaded
|
||||
default:
|
||||
return nil, fmt.Errorf("loading settings: %w", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Get returns a copy of the current settings.
|
||||
func (s *Store) Get() Settings {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cur
|
||||
}
|
||||
|
||||
// Set validates, persists, and swaps in the new settings.
|
||||
func (s *Store) Set(n Settings) error {
|
||||
if err := n.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := s.persist(n); err != nil {
|
||||
return err
|
||||
}
|
||||
s.cur = n
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) persist(n Settings) error {
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.Exec(`INSERT INTO settings (id, data) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`, string(b))
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user