summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-30 00:49:46 +0300
committerPaul Buetow <paul@buetow.org>2026-04-30 00:49:46 +0300
commit8982cda8afc40f3268bfc2614faa202fe6dea6d3 (patch)
tree6b81d52de95cd07283d285feb1a7b274cc1bc34e
parent77c405e1b28a74c1b94b8600d820178b054a5a08 (diff)
fix: seed random source in thumbnail generator
-rw-r--r--internal/thumb/thumb.go7
-rw-r--r--internal/thumb/thumb_test.go65
2 files changed, 68 insertions, 4 deletions
diff --git a/internal/thumb/thumb.go b/internal/thumb/thumb.go
index 96c496b..53a139b 100644
--- a/internal/thumb/thumb.go
+++ b/internal/thumb/thumb.go
@@ -6,6 +6,7 @@ import (
"fmt"
"math/rand"
"os/exec"
+ "time"
)
// Generator creates a thumbnail for a given media file.
@@ -16,12 +17,14 @@ type Generator interface {
// FFmpegGenerator uses ffmpeg to extract a random frame.
type FFmpegGenerator struct {
execer func(ctx context.Context, name string, arg ...string) *exec.Cmd
+ rnd *rand.Rand
}
-// NewFFmpegGenerator creates a new FFmpegGenerator.
+// NewFFmpegGenerator creates a new FFmpegGenerator with a seeded random source.
func NewFFmpegGenerator() *FFmpegGenerator {
return &FFmpegGenerator{
execer: exec.CommandContext,
+ rnd: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
@@ -30,7 +33,7 @@ func NewFFmpegGenerator() *FFmpegGenerator {
func (g *FFmpegGenerator) Generate(ctx context.Context, inputPath, outputPath string, duration float64) error {
offset := 0.0
if duration > 0 {
- offset = rand.Float64() * duration
+ offset = g.rnd.Float64() * duration
if offset < 1.0 {
offset = 1.0
}
diff --git a/internal/thumb/thumb_test.go b/internal/thumb/thumb_test.go
index 2cd6124..d6272a9 100644
--- a/internal/thumb/thumb_test.go
+++ b/internal/thumb/thumb_test.go
@@ -3,9 +3,12 @@ package thumb
import (
"context"
"errors"
+ "math/rand"
"os/exec"
+ "strconv"
"strings"
"testing"
+ "time"
)
func TestFFmpegGenerator_Generate(t *testing.T) {
@@ -35,7 +38,7 @@ func TestFFmpegGenerator_Generate(t *testing.T) {
return exec.Command("true")
}
- g := &FFmpegGenerator{execer: fakeExecer}
+ g := &FFmpegGenerator{execer: fakeExecer, rnd: rand.New(rand.NewSource(1))}
if err := g.Generate(ctx, "input.mp4", "output.jpg", 120.0); err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -58,12 +61,58 @@ func TestFFmpegGenerator_Generate_Error(t *testing.T) {
fakeExecer := func(_ context.Context, name string, arg ...string) *exec.Cmd {
return exec.Command("false")
}
- g := &FFmpegGenerator{execer: fakeExecer}
+ g := &FFmpegGenerator{execer: fakeExecer, rnd: rand.New(rand.NewSource(1))}
if err := g.Generate(ctx, "input.mp4", "output.jpg", 10.0); err == nil {
t.Fatal("expected error from failing ffmpeg command")
}
}
+func TestFFmpegGenerator_Generate_RandomOffset(t *testing.T) {
+ ctx := context.Background()
+ var offsets []float64
+ fakeExecer := func(_ context.Context, name string, arg ...string) *exec.Cmd {
+ for i := 0; i < len(arg); i++ {
+ if arg[i] == "-ss" && i+1 < len(arg) {
+ off, err := strconv.ParseFloat(arg[i+1], 64)
+ if err != nil {
+ t.Fatalf("failed to parse offset: %v", err)
+ }
+ offsets = append(offsets, off)
+ }
+ }
+ return exec.Command("true")
+ }
+
+ // Use two generators with different seeds.
+ g1 := &FFmpegGenerator{execer: fakeExecer, rnd: rand.New(rand.NewSource(time.Now().UnixNano()))}
+ g2 := &FFmpegGenerator{execer: fakeExecer, rnd: rand.New(rand.NewSource(time.Now().UnixNano() + 12345))}
+
+ for i := 0; i < 5; i++ {
+ if err := g1.Generate(ctx, "input.mp4", "out.jpg", 100.0); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if err := g2.Generate(ctx, "input.mp4", "out.jpg", 100.0); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ }
+
+ if len(offsets) != 10 {
+ t.Fatalf("expected 10 offsets, got %d", len(offsets))
+ }
+
+ // Check that not all offsets are identical (should be extremely unlikely with different seeds).
+ allSame := true
+ for i := 1; i < len(offsets); i++ {
+ if offsets[i] != offsets[0] {
+ allSame = false
+ break
+ }
+ }
+ if allSame {
+ t.Fatal("expected different random offsets, but all were identical")
+ }
+}
+
func TestMockGenerator(t *testing.T) {
ctx := context.Background()
m := &MockGenerator{}
@@ -78,3 +127,15 @@ func TestMockGenerator(t *testing.T) {
t.Fatal("expected error from mock generator")
}
}
+
+func TestNewFFmpegGenerator_Seeded(t *testing.T) {
+ g := NewFFmpegGenerator()
+ if g.rnd == nil {
+ t.Fatal("expected rnd to be initialized")
+ }
+ // Generate a value to ensure the source is functional.
+ v := g.rnd.Float64()
+ if v < 0 || v >= 1 {
+ t.Fatalf("expected float in [0,1), got %v", v)
+ }
+}