feat(playlists): track operations + cover-collage generator
AppendTracks / RemoveTrack / Reorder run in single transactions and update the denormalized rollups (track_count, duration_sec). Reorder uses a +10000 offset to bump every row out of the position range before writing the new positions, sidestepping PK conflicts during rewrite. GenerateCollage composes a 600x600 JPEG from the first 4 album covers, with a slate-tinted fallback for missing cells. Synchronous, called inline after every mutating operation. SVG-rasterized fallback is a follow-up — slice 1 ships with a solid placeholder.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package playlists_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/playlists"
|
||||
)
|
||||
|
||||
func TestGenerateCollage_EmptyPlaylist(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "alice")
|
||||
dir := t.TempDir()
|
||||
svc := playlists.NewService(pool, nil, dir)
|
||||
|
||||
pl, err := svc.Create(context.Background(), user.ID, "Empty", "", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
|
||||
relPath, err := playlists.GenerateCollage(context.Background(), pool, pl.ID, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCollage: %v", err)
|
||||
}
|
||||
if relPath != "" {
|
||||
t.Errorf("relPath = %q, want \"\" for empty playlist", relPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCollage_OneTrack(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
user := seedUser(t, pool, "alice")
|
||||
tk := seedTrack(t, pool, "Roygbiv", "BoC")
|
||||
dir := t.TempDir()
|
||||
svc := playlists.NewService(pool, nil, dir)
|
||||
|
||||
pl, err := svc.Create(context.Background(), user.ID, "One", "", false)
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
if err := svc.AppendTracks(context.Background(), user.ID, pl.ID, []pgtype.UUID{tk.ID}); err != nil {
|
||||
t.Fatalf("AppendTracks: %v", err)
|
||||
}
|
||||
|
||||
got, err := svc.Get(context.Background(), user.ID, pl.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if got.CoverPath == nil || *got.CoverPath == "" {
|
||||
t.Fatalf("CoverPath empty after append")
|
||||
}
|
||||
|
||||
full := filepath.Join(dir, *got.CoverPath)
|
||||
f, err := os.Open(full)
|
||||
if err != nil {
|
||||
t.Fatalf("open collage: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
img, format, err := image.Decode(f)
|
||||
if err != nil {
|
||||
t.Fatalf("decode collage: %v", err)
|
||||
}
|
||||
if format != "jpeg" {
|
||||
t.Errorf("format = %q, want jpeg", format)
|
||||
}
|
||||
if img.Bounds().Dx() != 600 || img.Bounds().Dy() != 600 {
|
||||
t.Errorf("dimensions = %dx%d, want 600x600", img.Bounds().Dx(), img.Bounds().Dy())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user