package coverart import ( "context" "image" "image/color" "image/jpeg" "os" "path/filepath" "testing" ) func TestExtractEmbeddedSuppliesExplicitOutputFormat(t *testing.T) { dir := t.TempDir() argsPath := filepath.Join(dir, "args") fakeFFmpeg := filepath.Join(dir, "ffmpeg") script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$ARGS_PATH\"\n" if err := os.WriteFile(fakeFFmpeg, []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("ARGS_PATH", argsPath) store := New(dir, fakeFFmpeg) path, err := store.ExtractEmbedded(context.Background(), filepath.Join(dir, "song.mp3"), 2) if err != nil { t.Fatalf("ExtractEmbedded: %v", err) } defer os.Remove(path) args, err := os.ReadFile(argsPath) if err != nil { t.Fatal(err) } got := string(args) if got != "-y\n-v\nerror\n-i\n"+filepath.Join(dir, "song.mp3")+"\n-map\n0:2\n-frames:v\n1\n-c:v\ncopy\n-f\nimage2\n"+path+"\n" { t.Fatalf("unexpected ffmpeg arguments:\n%s", got) } } func writeTestJPEG(t *testing.T, path string, w, h int) { t.Helper() img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := 0; y < h; y++ { for x := 0; x < w; x++ { img.Set(x, y, color.RGBA{R: uint8(x % 255), G: uint8(y % 255), B: 100, A: 255}) } } f, err := os.Create(path) if err != nil { t.Fatal(err) } defer f.Close() if err := jpeg.Encode(f, img, nil); err != nil { t.Fatal(err) } } func TestFindFolderImagePriority(t *testing.T) { dir := t.TempDir() writeTestJPEG(t, filepath.Join(dir, "folder.jpg"), 4, 4) writeTestJPEG(t, filepath.Join(dir, "cover.jpg"), 4, 4) got := FindFolderImage(dir) if filepath.Base(got) != "cover.jpg" { t.Errorf("FindFolderImage = %q, want cover.jpg to take priority over folder.jpg", got) } } func TestFindFolderImageNone(t *testing.T) { dir := t.TempDir() if got := FindFolderImage(dir); got != "" { t.Errorf("FindFolderImage = %q, want empty when no cover exists", got) } } func TestGetOrCreateResizesAndCaches(t *testing.T) { dir := t.TempDir() srcPath := filepath.Join(dir, "source.jpg") writeTestJPEG(t, srcPath, 800, 600) cacheDir := filepath.Join(dir, "cache") store := New(cacheDir, "") path1, err := store.GetOrCreate(srcPath, "hash1", 200) if err != nil { t.Fatalf("GetOrCreate: %v", err) } info1, err := os.Stat(path1) if err != nil { t.Fatalf("stat cached file: %v", err) } f, err := os.Open(path1) if err != nil { t.Fatal(err) } cfg, _, err := image.DecodeConfig(f) f.Close() if err != nil { t.Fatalf("decoding resized image: %v", err) } if cfg.Width != 200 { t.Errorf("resized width = %d, want 200", cfg.Width) } if cfg.Height >= 600 { t.Errorf("resized height = %d, should be scaled down proportionally", cfg.Height) } // Second call should hit the cache: same path, same mtime (not regenerated). path2, err := store.GetOrCreate(srcPath, "hash1", 200) if err != nil { t.Fatalf("GetOrCreate (2nd): %v", err) } if path1 != path2 { t.Errorf("cached path changed: %q -> %q", path1, path2) } info2, err := os.Stat(path2) if err != nil { t.Fatal(err) } if !info1.ModTime().Equal(info2.ModTime()) { t.Error("second GetOrCreate call regenerated the cached file instead of reusing it") } } func TestGetOrCreateSmallImageNotUpscaled(t *testing.T) { dir := t.TempDir() srcPath := filepath.Join(dir, "small.jpg") writeTestJPEG(t, srcPath, 50, 50) store := New(filepath.Join(dir, "cache"), "") path, err := store.GetOrCreate(srcPath, "hash-small", 500) if err != nil { t.Fatalf("GetOrCreate: %v", err) } f, err := os.Open(path) if err != nil { t.Fatal(err) } defer f.Close() cfg, _, err := image.DecodeConfig(f) if err != nil { t.Fatal(err) } if cfg.Width != 50 || cfg.Height != 50 { t.Errorf("size = %dx%d, want unscaled 50x50", cfg.Width, cfg.Height) } } func TestGetOrCreateRejectsOversizedSource(t *testing.T) { dir := t.TempDir() srcPath := filepath.Join(dir, "big.jpg") writeTestJPEG(t, srcPath, 200, 200) info, err := os.Stat(srcPath) if err != nil { t.Fatal(err) } store := New(filepath.Join(dir, "cache"), "") store.MaxSourceBytes = info.Size() - 1 // just under the real file size if _, err := store.GetOrCreate(srcPath, "hash-big", 50); err == nil { t.Fatal("expected GetOrCreate to reject a source image over MaxSourceBytes") } } func TestGetOrCreateAllowsUnderLimit(t *testing.T) { dir := t.TempDir() srcPath := filepath.Join(dir, "ok.jpg") writeTestJPEG(t, srcPath, 10, 10) store := New(filepath.Join(dir, "cache"), "") if _, err := store.GetOrCreate(srcPath, "hash-ok", 5); err != nil { t.Fatalf("GetOrCreate should succeed under the default limit: %v", err) } }