feat(server/playlists): generate cover collages for system playlists
System-generated playlists ("For You", "Songs like X") rendered with
empty placeholder boxes in the UI even when their contributing tracks
had album art. The build path inserted playlist rows directly without
ever invoking the existing playlists.GenerateCollage machinery —
that was only wired into user-edited playlist mutations.
After this change, BuildSystemPlaylists generates a 4-cell collage
for every system playlist it creates, post-commit. Same cover
mechanism user playlists use, same on-disk layout
(<DataDir>/playlist_covers/<id>.jpg). Cells whose source album lacks
art fall back to the FabledSword glyph, so a partially-covered
library still produces a sensible visual.
Threading: BuildSystemPlaylists, StartSystemPlaylistCron, and the
api lazy-build path all gain a dataDir string parameter; main.go
passes cfg.Storage.DataDir.
Drops the now-redundant PickTopAlbumCoverForArtistByUser lookup —
the collage is more visually informative than a single album cover
copy, and the seed-artist's top album is one of the contributing
tracks in the mix anyway, so it'll appear as one of the four cells.
Tests pass t.TempDir() for the dataDir parameter.
This commit is contained in:
@@ -71,7 +71,7 @@ func TestBuildSystemPlaylists_SufficientActivity(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
logger := discardLogger()
|
||||
u, _ := seedActiveLibrary(t, pool, "act1", 4, 3)
|
||||
if err := playlists.BuildSystemPlaylists(context.Background(), pool, logger, u.ID, time.Now().UTC()); err != nil {
|
||||
if err := playlists.BuildSystemPlaylists(context.Background(), pool, logger, u.ID, time.Now().UTC(), t.TempDir()); err != nil {
|
||||
t.Fatalf("build: %v", err)
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func TestBuildSystemPlaylists_QuarantineExcluded(t *testing.T) {
|
||||
quarantined := tracks[0]
|
||||
seedQuarantine(t, pool, u.ID, quarantined.ID)
|
||||
|
||||
if err := playlists.BuildSystemPlaylists(context.Background(), pool, logger, u.ID, time.Now().UTC()); err != nil {
|
||||
if err := playlists.BuildSystemPlaylists(context.Background(), pool, logger, u.ID, time.Now().UTC(), t.TempDir()); err != nil {
|
||||
t.Fatalf("build: %v", err)
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ func TestBuildSystemPlaylists_AtomicReplace(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// First build.
|
||||
if err := playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, time.Now().UTC()); err != nil {
|
||||
if err := playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, time.Now().UTC(), t.TempDir()); err != nil {
|
||||
t.Fatalf("first build: %v", err)
|
||||
}
|
||||
var ids1 []pgtype.UUID
|
||||
@@ -180,7 +180,7 @@ func TestBuildSystemPlaylists_AtomicReplace(t *testing.T) {
|
||||
}
|
||||
|
||||
// Second build — old rows must be gone, replaced with fresh ones.
|
||||
if err := playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, time.Now().UTC()); err != nil {
|
||||
if err := playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, time.Now().UTC(), t.TempDir()); err != nil {
|
||||
t.Fatalf("second build: %v", err)
|
||||
}
|
||||
rows2, _ := dbq.New(pool).ListPlaylistsByUserAndKind(ctx, dbq.ListPlaylistsByUserAndKindParams{
|
||||
@@ -204,8 +204,9 @@ func TestBuildSystemPlaylists_Concurrency(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() { defer wg.Done(); _ = playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, now) }()
|
||||
go func() { defer wg.Done(); _ = playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, now) }()
|
||||
dataDir := t.TempDir()
|
||||
go func() { defer wg.Done(); _ = playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, now, dataDir) }()
|
||||
go func() { defer wg.Done(); _ = playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, now, dataDir) }()
|
||||
wg.Wait()
|
||||
|
||||
// Exactly one run must have completed; in_flight must be false either way.
|
||||
@@ -228,12 +229,13 @@ func TestBuildSystemPlaylists_DailyNonceDeterminism(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
day1 := time.Date(2026, 5, 4, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
if err := playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, day1); err != nil {
|
||||
dataDir := t.TempDir()
|
||||
if err := playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, day1, dataDir); err != nil {
|
||||
t.Fatalf("build day1 first: %v", err)
|
||||
}
|
||||
snap1 := snapshotSystemTracks(t, pool, u.ID)
|
||||
|
||||
if err := playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, day1); err != nil {
|
||||
if err := playlists.BuildSystemPlaylists(ctx, pool, logger, u.ID, day1, dataDir); err != nil {
|
||||
t.Fatalf("build day1 second: %v", err)
|
||||
}
|
||||
snap2 := snapshotSystemTracks(t, pool, u.ID)
|
||||
|
||||
Reference in New Issue
Block a user