From 21421e4c78bdcad0c028e8a5b03aef66bb8fcc18 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Fri, 17 Apr 2026 15:17:01 -0700 Subject: [PATCH 1/2] Add new collections endpoint --- api/swagger/swagger-v1.yaml | 45 +++++++++++++ api/v1_playlists_new_releases.go | 64 ++++++++++++++++++ api/v1_playlists_new_releases_test.go | 94 +++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 api/v1_playlists_new_releases.go create mode 100644 api/v1_playlists_new_releases_test.go diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index 49625c6c..204fcaaf 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -1483,6 +1483,51 @@ paths: "500": description: Server error content: {} + /playlists/new-releases: + get: + tags: + - playlists + description: Returns recently released playlists or albums + operationId: Get Playlists New Releases + security: + - {} + - OAuth2: + - read + parameters: + - name: offset + in: query + description: + The number of items to skip. Useful for pagination (page number + * limit) + schema: + type: integer + - name: limit + in: query + description: The number of items to fetch + schema: + type: integer + - name: type + in: query + description: The type of content to filter by + schema: + type: string + default: playlist + enum: + - playlist + - album + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/trending_playlists_response" + "400": + description: Bad request + content: {} + "500": + description: Server error + content: {} /playlists/trending: get: tags: diff --git a/api/v1_playlists_new_releases.go b/api/v1_playlists_new_releases.go new file mode 100644 index 00000000..6fff6801 --- /dev/null +++ b/api/v1_playlists_new_releases.go @@ -0,0 +1,64 @@ +package api + +import ( + "api.audius.co/api/dbv1" + "github.com/gofiber/fiber/v2" + "github.com/jackc/pgx/v5" +) + +type GetPlaylistsNewReleasesParams struct { + Limit int `query:"limit" default:"10" validate:"min=1,max=100"` + Offset int `query:"offset" default:"0" validate:"min=0"` + Type string `query:"type" default:"playlist" validate:"oneof=playlist album"` +} + +func (app *ApiServer) v1PlaylistsNewReleases(c *fiber.Ctx) error { + params := GetPlaylistsNewReleasesParams{} + if err := app.ParseAndValidateQueryParams(c, ¶ms); err != nil { + return err + } + + isAlbum := params.Type == "album" + + sql := ` + SELECT playlist_id + FROM playlists + WHERE is_delete = false + AND is_current = true + AND is_private = false + AND is_album = @is_album + AND COALESCE(release_date, created_at) <= NOW() + AND COALESCE(release_date, created_at) > NOW() - INTERVAL '90 days' + ORDER BY COALESCE(release_date, created_at) DESC, playlist_id DESC + LIMIT @limit + OFFSET @offset + ` + + rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{ + "is_album": isAlbum, + "limit": params.Limit, + "offset": params.Offset, + }) + if err != nil { + return err + } + + ids, err := pgx.CollectRows(rows, pgx.RowTo[int32]) + if err != nil { + return err + } + + playlists, err := app.queries.Playlists(c.Context(), dbv1.PlaylistsParams{ + GetPlaylistsParams: dbv1.GetPlaylistsParams{ + Ids: ids, + MyID: app.getMyId(c), + }, + OmitTracks: true, + AuthedWallet: app.tryGetAuthedWallet(c), + }) + if err != nil { + return err + } + + return v1PlaylistsResponse(c, playlists) +} diff --git a/api/v1_playlists_new_releases_test.go b/api/v1_playlists_new_releases_test.go new file mode 100644 index 00000000..2051c6d7 --- /dev/null +++ b/api/v1_playlists_new_releases_test.go @@ -0,0 +1,94 @@ +package api + +import ( + "testing" + "time" + + "api.audius.co/api/dbv1" + "api.audius.co/database" + "api.audius.co/trashid" + "github.com/stretchr/testify/assert" +) + +func TestGetPlaylistsNewReleases_Albums(t *testing.T) { + app := emptyTestApp(t) + now := time.Now() + fixtures := database.FixtureMap{ + "users": {{"user_id": 1, "handle_lc": "one"}}, + "playlists": { + // newest album + { + "playlist_id": 1, + "playlist_owner_id": 1, + "is_album": true, + "is_private": false, + "release_date": now.AddDate(0, 0, -1), + "created_at": now.AddDate(0, 0, -1), + "playlist_name": "one", + }, + // older album + { + "playlist_id": 2, + "playlist_owner_id": 1, + "is_album": true, + "is_private": false, + "release_date": now.AddDate(0, 0, -10), + "created_at": now.AddDate(0, 0, -10), + "playlist_name": "two", + }, + // non-album playlist (excluded when type=album) + { + "playlist_id": 3, + "playlist_owner_id": 1, + "is_album": false, + "is_private": false, + "release_date": now, + "created_at": now, + "playlist_name": "three", + }, + // private album (excluded) + { + "playlist_id": 4, + "playlist_owner_id": 1, + "is_album": true, + "is_private": true, + "release_date": now, + "created_at": now, + "playlist_name": "four", + }, + // outside of 90-day window (excluded) + { + "playlist_id": 5, + "playlist_owner_id": 1, + "is_album": true, + "is_private": false, + "release_date": now.AddDate(0, 0, -120), + "created_at": now.AddDate(0, 0, -120), + "playlist_name": "five", + }, + // future release (excluded until release_date has passed) + { + "playlist_id": 6, + "playlist_owner_id": 1, + "is_album": true, + "is_private": false, + "release_date": now.AddDate(0, 0, 10), + "created_at": now, + "playlist_name": "six", + }, + }, + } + database.Seed(app.pool.Replicas[0], fixtures) + + var resp struct { + Data []dbv1.Playlist + } + + status, body := testGet(t, app, "/v1/playlists/new-releases?type=album", &resp) + assert.Equal(t, 200, status) + jsonAssert(t, body, map[string]any{ + "data.0.id": trashid.MustEncodeHashID(1), + "data.1.id": trashid.MustEncodeHashID(2), + }) + assert.Len(t, resp.Data, 2) +} From 37d1d16a1b9931800e8ca8ca11e63925ea7668f7 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Fri, 17 Apr 2026 15:40:24 -0700 Subject: [PATCH 2/2] Fix missing route --- api/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/server.go b/api/server.go index ea9fdecf..0cbd41e9 100644 --- a/api/server.go +++ b/api/server.go @@ -535,6 +535,7 @@ func NewApiServer(config config.Config) *ApiServer { g.Get("/playlists/unclaimed-id", app.v1PlaylistsUnclaimedId) g.Get("/playlists/trending", app.v1PlaylistsTrending) g.Get("/playlists/top", app.v1PlaylistsTop) + g.Get("/playlists/new-releases", app.v1PlaylistsNewReleases) g.Get("/playlists/by_permalink/:handle/:slug", app.v1PlaylistByPermalink) g.Get("/playlists/by-permalink/:handle/:slug", app.v1PlaylistByPermalink)