From aa88d0fab09ea0ff0f916ff632c756d882acd490 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:41:28 -0700 Subject: [PATCH 1/2] Include unlisted tracks in playlist responses Unlisted tracks added to playlists were being filtered out by the get_tracks query, causing a mismatch between playlist_contents and the hydrated tracks array. This sets IncludeUnlisted=true for both GET /v1/playlists/:id and GET /v1/playlists/:id/tracks. Co-Authored-By: Claude Opus 4.6 --- api/dbv1/parallel.go | 18 ++++++++++-------- api/dbv1/playlists.go | 9 +++++---- api/v1_playlist_tracks.go | 7 ++++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/api/dbv1/parallel.go b/api/dbv1/parallel.go index 8de1a37f..f7d17c45 100644 --- a/api/dbv1/parallel.go +++ b/api/dbv1/parallel.go @@ -7,11 +7,12 @@ import ( ) type ParallelParams struct { - UserIds []int32 - TrackIds []int32 - PlaylistIds []int32 - MyID int32 - AuthedWallet string + UserIds []int32 + TrackIds []int32 + PlaylistIds []int32 + MyID int32 + AuthedWallet string + IncludeUnlisted bool } type ParallelResult struct { @@ -43,9 +44,10 @@ func (q *Queries) Parallel(ctx context.Context, arg ParallelParams) (*ParallelRe var err error trackMap, err = q.TracksKeyed(ctx, TracksParams{ GetTracksParams: GetTracksParams{ - Ids: arg.TrackIds, - MyID: arg.MyID, - AuthedWallet: arg.AuthedWallet, + Ids: arg.TrackIds, + MyID: arg.MyID, + AuthedWallet: arg.AuthedWallet, + IncludeUnlisted: arg.IncludeUnlisted, }, }) return err diff --git a/api/dbv1/playlists.go b/api/dbv1/playlists.go index 6e90df8f..651b41af 100644 --- a/api/dbv1/playlists.go +++ b/api/dbv1/playlists.go @@ -69,10 +69,11 @@ func (q *Queries) PlaylistsKeyed(ctx context.Context, arg PlaylistsParams) (map[ // fetch users + tracks in parallel loaded, err := q.Parallel(ctx, ParallelParams{ - UserIds: userIds, - TrackIds: trackIds, - MyID: arg.MyID.(int32), - AuthedWallet: arg.AuthedWallet, + UserIds: userIds, + TrackIds: trackIds, + MyID: arg.MyID.(int32), + AuthedWallet: arg.AuthedWallet, + IncludeUnlisted: true, }) if err != nil { return nil, err diff --git a/api/v1_playlist_tracks.go b/api/v1_playlist_tracks.go index 55625ceb..2d25e845 100644 --- a/api/v1_playlist_tracks.go +++ b/api/v1_playlist_tracks.go @@ -58,9 +58,10 @@ func (app *ApiServer) v1PlaylistTracks(c *fiber.Ctx) error { tracks, err := app.queries.Tracks(c.Context(), dbv1.TracksParams{ GetTracksParams: dbv1.GetTracksParams{ - Ids: trackIds, - MyID: myId, - AuthedWallet: app.tryGetAuthedWallet(c), + Ids: trackIds, + MyID: myId, + AuthedWallet: app.tryGetAuthedWallet(c), + IncludeUnlisted: true, }, }) From 68b510e6d9e5707953624c227e92d711178646cf Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:47:01 -0700 Subject: [PATCH 2/2] Add tests for unlisted tracks in playlist responses Covers both GET /v1/playlists/:id (hydrated tracks array) and GET /v1/playlists/:id/tracks with unlisted track fixtures. Co-Authored-By: Claude Opus 4.6 --- api/v1_playlist_test.go | 56 ++++++++++++++++++++++++++++++ api/v1_playlist_tracks_test.go | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/api/v1_playlist_test.go b/api/v1_playlist_test.go index 223b0baa..7555d8a8 100644 --- a/api/v1_playlist_test.go +++ b/api/v1_playlist_test.go @@ -4,6 +4,7 @@ import ( "testing" "api.audius.co/api/dbv1" + "api.audius.co/database" "api.audius.co/trashid" "github.com/stretchr/testify/assert" ) @@ -99,3 +100,58 @@ func TestGetPlaylistUsdcPurchaseSelfAccess(t *testing.T) { "data.0.access": `{"stream":true,"download":true}`, }) } + +func TestGetPlaylistIncludesUnlistedTracks(t *testing.T) { + app := emptyTestApp(t) + + fixtures := database.FixtureMap{ + "users": []map[string]any{ + { + "user_id": 1, + "handle": "user1", + "name": "User 1", + }, + }, + "tracks": []map[string]any{ + { + "track_id": 1, + "owner_id": 1, + "title": "Listed Track", + }, + { + "track_id": 2, + "owner_id": 1, + "title": "Unlisted Track", + "is_unlisted": true, + }, + }, + "playlists": []map[string]any{ + { + "playlist_id": 1, + "playlist_owner_id": 1, + "playlist_contents": map[string]any{ + "track_ids": []map[string]any{ + {"track": 1, "time": 1, "metadata_time": 1}, + {"track": 2, "time": 2, "metadata_time": 2}, + }, + }, + }, + }, + } + database.Seed(app.pool.Replicas[0], fixtures) + + playlistId := trashid.MustEncodeHashID(1) + + var playlistResponse struct { + Data []dbv1.Playlist + } + status, body := testGet(t, app, "/v1/full/playlists/"+playlistId, &playlistResponse) + assert.Equal(t, 200, status) + + // Both listed and unlisted tracks should appear in the hydrated tracks array + jsonAssert(t, body, map[string]any{ + "data.0.tracks.#": 2, + "data.0.tracks.0.id": trashid.MustEncodeHashID(1), + "data.0.tracks.1.id": trashid.MustEncodeHashID(2), + }) +} diff --git a/api/v1_playlist_tracks_test.go b/api/v1_playlist_tracks_test.go index f38b3914..926ead1e 100644 --- a/api/v1_playlist_tracks_test.go +++ b/api/v1_playlist_tracks_test.go @@ -88,3 +88,65 @@ func TestV1PlaylistTracks(t *testing.T) { }) } } + +func TestV1PlaylistTracksIncludesUnlisted(t *testing.T) { + app := emptyTestApp(t) + + fixtures := database.FixtureMap{ + "users": []map[string]any{ + { + "user_id": 1, + "handle": "user1", + "name": "User 1", + }, + }, + "tracks": []map[string]any{ + { + "track_id": 1, + "owner_id": 1, + "title": "Listed Track", + }, + { + "track_id": 2, + "owner_id": 1, + "title": "Unlisted Track", + "is_unlisted": true, + }, + }, + "playlists": []map[string]any{ + { + "playlist_id": 1, + "playlist_owner_id": 1, + "playlist_contents": map[string]any{ + "track_ids": []map[string]any{ + {"track": 1, "time": 1, "metadata_time": 1}, + {"track": 2, "time": 2, "metadata_time": 2}, + }, + }, + }, + }, + } + database.Seed(app.pool.Replicas[0], fixtures) + + playlistId := trashid.MustEncodeHashID(1) + + // GET /v1/playlists/:id/tracks should include unlisted tracks + { + status, body := testGet(t, app, "/v1/playlists/"+playlistId+"/tracks", nil) + assert.Equal(t, 200, status) + jsonAssert(t, body, map[string]any{ + "data.#": 2, + "data.0.id": trashid.MustEncodeHashID(1), + "data.1.id": trashid.MustEncodeHashID(2), + }) + } + + // Also works with exclude_gated=false + { + status, body := testGet(t, app, "/v1/playlists/"+playlistId+"/tracks?exclude_gated=false", nil) + assert.Equal(t, 200, status) + jsonAssert(t, body, map[string]any{ + "data.#": 2, + }) + } +}