summaryrefslogtreecommitdiff
path: root/internal/thumb
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-17 15:25:52 +0300
committerPaul Buetow <paul@buetow.org>2026-05-17 15:25:52 +0300
commit914bd7cd6aa14e839332a98d91c30b19865b0cf2 (patch)
tree02b11537237a204048589e14ed24938b805e42f7 /internal/thumb
parent3b24f0e1be832584d6550e8cc3e5d24329f66f90 (diff)
Restructure repo: move Go server into player-server/
Diffstat (limited to 'internal/thumb')
-rw-r--r--internal/thumb/thumb.go80
-rw-r--r--internal/thumb/thumb_test.go151
2 files changed, 0 insertions, 231 deletions
diff --git a/internal/thumb/thumb.go b/internal/thumb/thumb.go
deleted file mode 100644
index 32907b4..0000000
--- a/internal/thumb/thumb.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Package thumb generates thumbnail images.
-package thumb
-
-import (
- "context"
- "fmt"
- "math/rand"
- "os/exec"
- "time"
-)
-
-// Generator creates a thumbnail for a given media file.
-type Generator interface {
- Generate(ctx context.Context, inputPath, outputPath string, duration float64) error
-}
-
-// 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
-}
-
-var _ Generator = (*FFmpegGenerator)(nil)
-
-// 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())),
- }
-}
-
-const thumbWaitDelay = 15 * time.Second
-
-// Generate picks a random offset (at least 1 second if duration > 0) and
-// runs ffmpeg to produce a JPEG thumbnail.
-func (g *FFmpegGenerator) Generate(ctx context.Context, inputPath, outputPath string, duration float64) error {
- offset := 0.0
- if duration > 0 {
- offset = g.rnd.Float64() * duration
- if offset < 1.0 {
- offset = 1.0
- }
- }
-
- // Build args: only add -ss when we have a real duration (video).
- // For static images, -ss before -i produces no output frame on some
- // ffmpeg versions (it skips past the single image2 frame).
- args := []string{"-i", inputPath}
- if duration > 0 {
- // Prepend -ss before -i for fast seek when we have a video.
- args = append([]string{"-ss", fmt.Sprintf("%.3f", offset)}, args...)
- }
- args = append(args,
- "-vf", "scale=320:-1",
- "-frames:v", "1",
- "-q:v", "2",
- "-y",
- outputPath,
- )
- cmd := g.execer(ctx, "ffmpeg", args...)
- cmd.WaitDelay = thumbWaitDelay
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("ffmpeg generate thumbnail for %s: %w", inputPath, err)
- }
- return nil
-}
-
-// MockGenerator is a test fake for Generator.
-type MockGenerator struct {
- GenerateFunc func(ctx context.Context, inputPath, outputPath string, duration float64) error
-}
-
-// Generate delegates to GenerateFunc or succeeds silently.
-func (m *MockGenerator) Generate(ctx context.Context, inputPath, outputPath string, duration float64) error {
- if m.GenerateFunc != nil {
- return m.GenerateFunc(ctx, inputPath, outputPath, duration)
- }
- return nil
-}
diff --git a/internal/thumb/thumb_test.go b/internal/thumb/thumb_test.go
deleted file mode 100644
index 3ed9dfd..0000000
--- a/internal/thumb/thumb_test.go
+++ /dev/null
@@ -1,151 +0,0 @@
-package thumb
-
-import (
- "context"
- "errors"
- "math/rand"
- "os/exec"
- "strconv"
- "strings"
- "testing"
- "time"
-)
-
-func TestFFmpegGenerator_Generate(t *testing.T) {
- ctx := context.Background()
-
- // Video with duration > 0 should include -ss.
- videoCalled := false
- fakeExecerVideo := func(_ context.Context, name string, arg ...string) *exec.Cmd {
- videoCalled = true
- if name != "ffmpeg" {
- t.Errorf("expected ffmpeg, got %s", name)
- }
- args := strings.Join(arg, " ")
- if !strings.Contains(args, "-ss") {
- t.Error("missing -ss flag")
- }
- if !strings.Contains(args, "-i") {
- t.Error("missing -i flag")
- }
- if !strings.Contains(args, "-frames:v 1") {
- t.Error("missing -frames:v 1 flag")
- }
- if !strings.Contains(args, "-y") {
- t.Error("missing -y flag")
- }
- return exec.Command("true")
- }
- g := &FFmpegGenerator{execer: fakeExecerVideo, rnd: rand.New(rand.NewSource(1))}
- if err := g.Generate(ctx, "input.mp4", "out.jpg", 120.0); err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if !videoCalled {
- t.Fatal("expected fake execer to be called")
- }
-
- // Image with duration == 0 should NOT include -ss.
- imgCalled := false
- fakeExecerImage := func(_ context.Context, name string, arg ...string) *exec.Cmd {
- imgCalled = true
- args := strings.Join(arg, " ")
- if strings.Contains(args, "-ss") {
- t.Error("unexpected -ss flag for image")
- }
- if !strings.Contains(args, "-i") {
- t.Error("missing -i flag")
- }
- return exec.Command("true")
- }
- gi := &FFmpegGenerator{execer: fakeExecerImage, rnd: rand.New(rand.NewSource(1))}
- if err := gi.Generate(ctx, "photo.jpg", "thumb.jpg", 0); err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if !imgCalled {
- t.Fatal("expected fake execer to be called for image")
- }
-}
-
-func TestFFmpegGenerator_Generate_Error(t *testing.T) {
- ctx := context.Background()
- fakeExecer := func(_ context.Context, name string, arg ...string) *exec.Cmd {
- return exec.Command("false")
- }
- 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{}
- if err := m.Generate(ctx, "in", "out", 0); err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
-
- m.GenerateFunc = func(context.Context, string, string, float64) error {
- return errors.New("fail")
- }
- if err := m.Generate(ctx, "in", "out", 0); err == nil {
- 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)
- }
-}