add: live encode progress tracking and read-only status web UI

Stream ffmpeg -progress during the video encode into an in-memory tracker
(internal/status) so the long-running step is no longer a black box: percent,
fps, speed, and ETA are derived from the source duration and updated ~1/s.
Progress prints to stdout only (no logs.db row) to avoid burying real events.

Expose it over HTTP (internal/server, default :8080, set http_addr to "" to
disable): GET /status returns the live snapshot, the pending input queue, and
recent non-debug events; GET / serves an embedded dashboard that polls /status
every second. The server shuts down on the same SIGINT/SIGTERM context as the
watcher.
This commit is contained in:
Esa Kataja
2026-06-21 17:43:00 +03:00
parent 4147cb9470
commit aa8a341069
10 changed files with 740 additions and 8 deletions
+29 -3
View File
@@ -6,18 +6,22 @@ import (
"encoding/hex"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
"videnc-vibe/internal/config"
"videnc-vibe/internal/encoder"
"videnc-vibe/internal/logger"
"videnc-vibe/internal/metadata"
"videnc-vibe/internal/mover"
"videnc-vibe/internal/server"
"videnc-vibe/internal/status"
"videnc-vibe/internal/watcher"
"videnc-vibe/pkg/types"
)
@@ -53,7 +57,8 @@ func main() {
}
defer log.Close()
enc := encoder.New(log)
tracker := status.New()
enc := encoder.New(log, tracker)
if err := enc.CheckDeps(); err != nil {
log.Error("Dependency check failed", err.Error())
fmt.Fprintf(os.Stderr, "Dependency check failed: %v\n", err)
@@ -65,17 +70,38 @@ func main() {
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()}
go func() {
log.Info(fmt.Sprintf("Status server listening on %s", addr))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error("Status server", err.Error())
}
}()
go func() {
<-ctx.Done()
shutCtx, shutCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutCancel()
_ = srv.Shutdown(shutCtx)
}()
}
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)
return processFile(ctx, inputPath, cfg, enc, metaClient, log, tracker)
})
log.Info("videnc-vibe started")
}
func processFile(ctx context.Context, inputPath string, cfg *types.Config, enc *encoder.Encoder, metaClient *metadata.Client, log *logger.Logger) error {
func processFile(ctx context.Context, inputPath string, cfg *types.Config, enc *encoder.Encoder, metaClient *metadata.Client, log *logger.Logger, tracker *status.Tracker) error {
log.Info(fmt.Sprintf("Processing: %s", inputPath))
// 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)
defer tracker.Idle()
filename := filepath.Base(inputPath)
isSeries, imdbID, tvmazeID, season, episode := metadata.ParseFilename(filename)