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:
Esa Kataja
2026-06-21 19:51:35 +03:00
parent bef58f480b
commit 8fc00c96b9
8 changed files with 427 additions and 19 deletions
+33 -17
View File
@@ -21,6 +21,7 @@ import (
"av1dae/internal/metadata"
"av1dae/internal/mover"
"av1dae/internal/server"
"av1dae/internal/settings"
"av1dae/internal/status"
"av1dae/internal/watcher"
"av1dae/pkg/types"
@@ -65,13 +66,33 @@ func main() {
os.Exit(1)
}
// Live-editable settings: config.yaml seeds the store on first run; after
// that the DB (logs.db) is the source of truth for these values.
store, err := settings.New(log.DB(), settings.Settings{
DVD: settings.Profile{CRF: cfg.Encoding.DVD.CRF, Preset: cfg.Encoding.DVD.Preset},
Bluray: settings.Profile{CRF: cfg.Encoding.Bluray.CRF, Preset: cfg.Encoding.Bluray.Preset},
WebDL: settings.Profile{CRF: cfg.Encoding.WebDL.CRF, Preset: cfg.Encoding.WebDL.Preset},
TVRip: settings.Profile{CRF: cfg.Encoding.TVRip.CRF, Preset: cfg.Encoding.TVRip.Preset},
LP: cfg.Encoding.LP,
OMDBAPIKey: cfg.OMDBAPIKey,
LogRetentionDays: cfg.LogRetentionDays,
DeleteOriginals: deleteOrigin,
})
if err != nil {
log.Error("Settings init failed", err.Error())
fmt.Fprintf(os.Stderr, "Failed to init settings: %v\n", err)
os.Exit(1)
}
// A persisted retention value (DB) overrides the config-seeded one.
log.SetRetention(store.Get().LogRetentionDays)
metaClient := metadata.NewClient(cfg.OMDBAPIKey, log)
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
if addr := *cfg.HTTPAddr; addr != "" {
srv := &http.Server{Addr: addr, Handler: server.New(tracker, log, cfg.Paths.Input).Handler()}
srv := &http.Server{Addr: addr, Handler: server.New(tracker, log, store, cfg.Paths.Input).Handler()}
go func() {
log.Info(fmt.Sprintf("Status server listening on %s", addr))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
@@ -88,15 +109,20 @@ func main() {
w := watcher.New(cfg.Paths.Input, 15)
w.Start(ctx, func(ctx context.Context, inputPath string) error {
return processFile(ctx, inputPath, cfg, enc, metaClient, log, tracker)
return processFile(ctx, inputPath, cfg, enc, metaClient, log, tracker, store)
})
log.Info("av1dae started")
}
func processFile(ctx context.Context, inputPath string, cfg *types.Config, enc *encoder.Encoder, metaClient *metadata.Client, log *logger.Logger, tracker *status.Tracker) error {
func processFile(ctx context.Context, inputPath string, cfg *types.Config, enc *encoder.Encoder, metaClient *metadata.Client, log *logger.Logger, tracker *status.Tracker, store *settings.Store) error {
log.Info(fmt.Sprintf("Processing: %s", inputPath))
// Snapshot the live settings once for the whole job, so a save mid-encode
// doesn't change anything until the next file.
cur := store.Get()
metaClient.SetAPIKey(cur.OMDBAPIKey)
// Mark this file as the active job (phase: probing) and clear the tracker
// back to idle on every exit path — success, failure, or cancellation.
tracker.Begin(inputPath)
@@ -136,17 +162,7 @@ func processFile(ctx context.Context, inputPath string, cfg *types.Config, enc *
mediaType = watcher.DetectMediaType(width, height)
}
var profile types.EncodingParams
switch mediaType {
case types.MediaTypeBluRay:
profile = cfg.Encoding.Bluray
case types.MediaTypeWebDL:
profile = cfg.Encoding.WebDL
case types.MediaTypeTVRip:
profile = cfg.Encoding.TVRip
default:
profile = cfg.Encoding.DVD
}
profile := cur.ProfileFor(mediaType)
crf := profile.CRF
preset := profile.Preset
@@ -192,8 +208,8 @@ func processFile(ctx context.Context, inputPath string, cfg *types.Config, enc *
MediaType: mediaType,
CRF: crf,
Preset: preset,
LP: cfg.Encoding.LP,
DeleteOrigin: deleteOrigin,
LP: cur.LP,
DeleteOrigin: cur.DeleteOriginals,
}
if err := enc.Transcode(ctx, inputPath, workDir, job, meta, interlaced, streamLangs); err != nil {
@@ -224,7 +240,7 @@ func processFile(ctx context.Context, inputPath string, cfg *types.Config, enc *
return err
}
if deleteOrigin {
if cur.DeleteOriginals {
mover.Delete(inputPath)
log.Info(fmt.Sprintf("Deleted original: %s", inputPath))
} else {