fix(server/playlists): dedupe covers in playlist cover collage
The collage builder pulled exactly 4 tracks (in playlist position order) and rendered each track's album cover into a 2x2 cell. A playlist where the first 4 tracks share an album rendered as four identical cells of the same cover; a "Songs like X" mix where two similar artists dominated produced very similar collages. Now pulls 50 rows from the same query and drops adjacent / duplicate covers in Go before passing to the renderer. NULL and empty cover_art_paths are NOT deduped (they each render the fallback glyph in their own cell — the right behavior for a partially-covered playlist). Preserves playlist position order: the first occurrence of each unique cover wins. Five unit tests cover the behavior: drops duplicates, keeps NULLs, handles empty-string paths, preserves order, mixed null/dup case. Tangential to the For-You CTA slice but lands here as a small quality-of-life fix that benefits every playlist's collage.
This commit is contained in:
@@ -33,13 +33,20 @@ const (
|
||||
// "" when the playlist is empty (cover_path also cleared).
|
||||
func GenerateCollage(ctx context.Context, pool *pgxpool.Pool, playlistID pgtype.UUID, dataDir string) (string, error) {
|
||||
q := dbq.New(pool)
|
||||
rows, err := q.ListAllPlaylistTracksForCollage(ctx, dbq.ListAllPlaylistTracksForCollageParams{
|
||||
// Pull more rows than cells so we can dedupe identical covers and
|
||||
// still fill every cell. A 25-track "Songs like X" playlist that
|
||||
// has 5 tracks each from 5 albums shouldn't render as four
|
||||
// identical cells of the first album's cover; pulling 50 and
|
||||
// deduping by cover_art_path gives us four distinct covers in
|
||||
// realistic libraries.
|
||||
allRows, err := q.ListAllPlaylistTracksForCollage(ctx, dbq.ListAllPlaylistTracksForCollageParams{
|
||||
PlaylistID: playlistID,
|
||||
Limit: 4,
|
||||
Limit: 50,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("list collage tracks: %w", err)
|
||||
}
|
||||
rows := dedupeCollageRows(allRows, 4)
|
||||
|
||||
if len(rows) == 0 {
|
||||
// Empty playlist: clear cover_path and remove any stale file.
|
||||
@@ -101,6 +108,34 @@ func GenerateCollage(ctx context.Context, pool *pgxpool.Pool, playlistID pgtype.
|
||||
return relPath, nil
|
||||
}
|
||||
|
||||
// dedupeCollageRows trims the input to at most max rows such that no
|
||||
// non-empty cover_art_path appears twice. Rows with NULL or empty
|
||||
// cover_art_path are kept as-is — they each render the fallback glyph
|
||||
// in their own cell, which is the right behavior (a partially-covered
|
||||
// playlist still gets a sensible visual rather than collapsing every
|
||||
// missing cover into a single cell).
|
||||
//
|
||||
// Preserves input order (which is playlist position order from the SQL).
|
||||
func dedupeCollageRows(rows []dbq.ListAllPlaylistTracksForCollageRow, max int) []dbq.ListAllPlaylistTracksForCollageRow {
|
||||
seen := make(map[string]bool, max)
|
||||
out := make([]dbq.ListAllPlaylistTracksForCollageRow, 0, max)
|
||||
for _, r := range rows {
|
||||
if len(out) >= max {
|
||||
break
|
||||
}
|
||||
if r.AlbumCoverPath == nil || *r.AlbumCoverPath == "" {
|
||||
out = append(out, r)
|
||||
continue
|
||||
}
|
||||
if seen[*r.AlbumCoverPath] {
|
||||
continue
|
||||
}
|
||||
seen[*r.AlbumCoverPath] = true
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// loadCellImage tries to load the album cover at the given path. On any
|
||||
// failure (empty path, missing file, decode error), returns the rendered
|
||||
// fallback glyph as a 300x300 image.
|
||||
|
||||
Reference in New Issue
Block a user