diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-09 22:02:22 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-09 22:02:22 +0300 |
| commit | d4d79e17433858d27846774274e38d907621b184 (patch) | |
| tree | 7d14ff6569689493331ee244313bda630a5b7b76 /internal/probe | |
| parent | 923c09dd5d9798709b5b942f127f1e2189d8dae6 (diff) | |
Harden external process and network calls
- Add cmd.WaitDelay (10-15s) to ffprobe, ffmpeg remux, and thumbnail generation.
- Add bounded retries with exponential backoff to FFProber.Probe (max 3 attempts).
- Inject *http.Client into podcast ParseFeed, replacing gofeed's default client.
- Update all callers (podcast service, tests, integration tests) to pass client.
- Ensure go test ./... -race -cover passes.
Diffstat (limited to 'internal/probe')
| -rw-r--r-- | internal/probe/probe.go | 65 | ||||
| -rw-r--r-- | internal/probe/remux.go | 4 |
2 files changed, 65 insertions, 4 deletions
diff --git a/internal/probe/probe.go b/internal/probe/probe.go index e94d2e5..a3fce78 100644 --- a/internal/probe/probe.go +++ b/internal/probe/probe.go @@ -4,31 +4,87 @@ package probe import ( "context" "encoding/json" + "errors" "fmt" "os" "os/exec" "strconv" + "time" "codeberg.org/snonux/player/internal/mediatype" "codeberg.org/snonux/player/internal/model" "github.com/rwcarlsen/goexif/exif" ) +const ( + defaultProbeWaitDelay = 10 * time.Second + defaultProbeMaxRetries = 2 + defaultProbeRetryDelay = 500 * time.Millisecond +) + // Prober extracts metadata from a media file. type Prober interface { Probe(ctx context.Context, path string) (*model.Metadata, error) } // FFProber wraps the ffprobe command-line tool. -type FFProber struct{} +type FFProber struct { + maxRetries int + retryDelay time.Duration + waitDelay time.Duration +} -// NewFFProber creates a new FFProber. +// NewFFProber creates a new FFProber with bounded retries and a process wait delay. func NewFFProber() *FFProber { - return &FFProber{} + return &FFProber{ + maxRetries: defaultProbeMaxRetries, + retryDelay: defaultProbeRetryDelay, + waitDelay: defaultProbeWaitDelay, + } } -// Probe runs ffprobe against the given path and parses the resulting JSON. +// Probe runs ffprobe against the given path with retries and parses the resulting JSON. func (f *FFProber) Probe(ctx context.Context, path string) (*model.Metadata, error) { + var lastErr error + attempts := f.maxRetries + 1 + if attempts <= 0 { + attempts = 1 + } + + for i := 0; i < attempts; i++ { + if err := ctx.Err(); err != nil { + return nil, err + } + + meta, err := f.probeOnce(ctx, path) + if err == nil { + return meta, nil + } + lastErr = err + + // Don't retry on context cancellation. + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + break + } + + if i < attempts-1 { + delay := f.retryDelay * time.Duration(1<<i) + const maxDelay = 30 * time.Second + if delay > maxDelay { + delay = maxDelay + } + select { + case <-time.After(delay): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + } + return nil, lastErr +} + +// probeOnce performs a single ffprobe invocation. +func (f *FFProber) probeOnce(ctx context.Context, path string) (*model.Metadata, error) { cmd := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_format", @@ -36,6 +92,7 @@ func (f *FFProber) Probe(ctx context.Context, path string) (*model.Metadata, err "-of", "json", path, ) + cmd.WaitDelay = f.waitDelay out, err := cmd.Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok && len(exitErr.Stderr) > 0 { diff --git a/internal/probe/remux.go b/internal/probe/remux.go index 35e87d3..07870f8 100644 --- a/internal/probe/remux.go +++ b/internal/probe/remux.go @@ -8,6 +8,7 @@ import ( "log/slog" "os" "os/exec" + "time" ) // Remuxer remuxes media on-the-fly to a browser-friendly container. @@ -23,6 +24,8 @@ func NewFFRemuxer() *FFRemuxer { return &FFRemuxer{} } +const remuxWaitDelay = 10 * time.Second + // Remux runs ffmpeg to copy video/audio streams into a fragmented MP4 // suitable for streaming to a browser. func (f *FFRemuxer) Remux(ctx context.Context, inputPath string, w io.Writer) error { @@ -43,6 +46,7 @@ func (f *FFRemuxer) Remux(ctx context.Context, inputPath string, w io.Writer) er "pipe:1", ) cmd.Stderr = os.Stderr + cmd.WaitDelay = remuxWaitDelay stdout, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("remux stdout pipe: %w", err) |
