From 914bd7cd6aa14e839332a98d91c30b19865b0cf2 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 17 May 2026 15:25:52 +0300 Subject: Restructure repo: move Go server into player-server/ --- internal/thumb/thumb.go | 80 ----------------------- internal/thumb/thumb_test.go | 151 ------------------------------------------- 2 files changed, 231 deletions(-) delete mode 100644 internal/thumb/thumb.go delete mode 100644 internal/thumb/thumb_test.go (limited to 'internal/thumb') 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) - } -} -- cgit v1.2.3