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:
+33
-17
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user