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
131 lines
3.6 KiB
Go
131 lines
3.6 KiB
Go
// 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
|
|
}
|