summaryrefslogtreecommitdiff
path: root/internal/probe
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-09 22:02:22 +0300
committerPaul Buetow <paul@buetow.org>2026-05-09 22:02:22 +0300
commitd4d79e17433858d27846774274e38d907621b184 (patch)
tree7d14ff6569689493331ee244313bda630a5b7b76 /internal/probe
parent923c09dd5d9798709b5b942f127f1e2189d8dae6 (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.go65
-rw-r--r--internal/probe/remux.go4
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)