diff --git a/api/server.go b/api/server.go index 0757338f..9eaccff5 100644 --- a/api/server.go +++ b/api/server.go @@ -474,7 +474,8 @@ func NewApiServer(config config.Config) *ApiServer { g.Get("/tracks/search", app.v1TracksSearch) g.Get("/tracks/unclaimed_id", app.v1TracksUnclaimedId) - g.Get("/tracks/trending", app.v1TracksTrending) + g.Get("/tracks/latest", app.v1TracksLatest) + g.Get("/tracks/trending", app.v1TracksTrending) g.Get("/tracks/trending/ids", app.v1TracksTrendingIds) g.Get("/tracks/trending/winners", app.v1TracksTrendingWinners) g.Get("/tracks/trending/underground", app.v1TracksTrendingUnderground) diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index 55160766..4ea16c90 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -2760,6 +2760,52 @@ paths: "500": description: Server error content: {} + /tracks/latest: + get: + tags: + - tracks + description: Gets the most recent tracks on Audius, ordered by creation date + operationId: Get Latest Tracks + 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: user_id + in: query + description: The user ID of the user making the request + schema: + type: string + - name: genre + in: query + description: Filter to a specified genre + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/tracks_response" + "400": + description: Bad request + content: {} + "500": + description: Server error + content: {} /tracks/trending: get: tags: diff --git a/api/v1_tracks_latest.go b/api/v1_tracks_latest.go new file mode 100644 index 00000000..95b5d37a --- /dev/null +++ b/api/v1_tracks_latest.go @@ -0,0 +1,70 @@ +package api + +import ( + "api.audius.co/api/dbv1" + "github.com/gofiber/fiber/v2" + "github.com/jackc/pgx/v5" +) + +type GetLatestTracksParams struct { + Limit int `query:"limit" default:"100" validate:"min=1,max=100"` + Offset int `query:"offset" default:"0" validate:"min=0,max=200"` + Genre string `query:"genre" default:""` +} + +func (app *ApiServer) v1TracksLatest(c *fiber.Ctx) error { + var params GetLatestTracksParams + if err := app.ParseAndValidateQueryParams(c, ¶ms); err != nil { + return err + } + + myId := app.getMyId(c) + + trackIds, err := app.getLatestTrackIds(c, params.Genre, params.Limit, params.Offset) + if err != nil { + return err + } + + tracks, err := app.queries.Tracks(c.Context(), dbv1.TracksParams{ + GetTracksParams: dbv1.GetTracksParams{ + Ids: trackIds, + MyID: myId, + AuthedWallet: app.tryGetAuthedWallet(c), + }, + }) + if err != nil { + return err + } + + return v1TracksResponse(c, tracks) +} + +func (app *ApiServer) getLatestTrackIds(c *fiber.Ctx, genre string, limit int, offset int) ([]int32, error) { + sql := ` + SELECT track_id + FROM tracks + WHERE is_current = true + AND is_delete = false + AND is_unlisted = false + AND is_available = true + AND (@genre = '' OR genre = @genre) + ORDER BY + created_at DESC, + track_id DESC + LIMIT @limit + OFFSET @offset + ` + + args := pgx.NamedArgs{ + "genre": genre, + "limit": limit, + "offset": offset, + } + + rows, err := app.pool.Query(c.Context(), sql, args) + if err != nil { + return nil, err + } + + return pgx.CollectRows(rows, pgx.RowTo[int32]) +} diff --git a/api/v1_tracks_latest_test.go b/api/v1_tracks_latest_test.go new file mode 100644 index 00000000..a3aa6712 --- /dev/null +++ b/api/v1_tracks_latest_test.go @@ -0,0 +1,74 @@ +package api + +import ( + "testing" + + "api.audius.co/api/dbv1" + "api.audius.co/database" + "api.audius.co/trashid" + "github.com/stretchr/testify/assert" +) + +func latestTestApp(t *testing.T) *ApiServer { + app := testAppWithFixtures(t) + database.SeedTable(app.pool.Replicas[0], "tracks", []map[string]any{ + {"track_id": 800, "genre": "LatestTestGenreA", "owner_id": 1, "title": "Latest Track Old", "is_unlisted": "f", "created_at": "2025-01-01 00:00:00"}, + {"track_id": 801, "genre": "LatestTestGenreA", "owner_id": 1, "title": "Latest Track Mid", "is_unlisted": "f", "created_at": "2025-02-01 00:00:00"}, + {"track_id": 802, "genre": "LatestTestGenreC", "owner_id": 1, "title": "Latest Track New", "is_unlisted": "f", "created_at": "2025-03-01 00:00:00"}, + {"track_id": 803, "genre": "LatestTestGenreB", "owner_id": 1, "title": "Latest Track Visible", "is_unlisted": "f", "created_at": "2025-02-15 00:00:00"}, + {"track_id": 804, "genre": "LatestTestGenreB", "owner_id": 1, "title": "Latest Track Hidden", "is_unlisted": "t", "created_at": "2025-02-20 00:00:00"}, + }) + return app +} + +func TestGetLatest(t *testing.T) { + app := latestTestApp(t) + var resp struct { + Data []dbv1.Track + } + status, _ := testGet(t, app, "/v1/tracks/latest?limit=5", &resp) + assert.Equal(t, 200, status) + assert.Equal(t, 5, len(resp.Data)) +} + +func TestGetLatestWithGenre(t *testing.T) { + app := latestTestApp(t) + var resp struct { + Data []dbv1.Track + } + status, _ := testGet(t, app, "/v1/tracks/latest?genre=LatestTestGenreA", &resp) + assert.Equal(t, 200, status) + assert.Equal(t, 2, len(resp.Data)) + assert.Equal(t, trashid.MustEncodeHashID(801), resp.Data[0].ID) + assert.Equal(t, trashid.MustEncodeHashID(800), resp.Data[1].ID) + for _, track := range resp.Data { + assert.Equal(t, "LatestTestGenreA", track.Genre.String) + } +} + +func TestGetLatestWithLimitOffset(t *testing.T) { + app := latestTestApp(t) + var resp struct { + Data []dbv1.Track + } + status, _ := testGet(t, app, "/v1/tracks/latest?genre=LatestTestGenreA&limit=1&offset=0", &resp) + assert.Equal(t, 200, status) + assert.Equal(t, 1, len(resp.Data)) + assert.Equal(t, trashid.MustEncodeHashID(801), resp.Data[0].ID) + + status, _ = testGet(t, app, "/v1/tracks/latest?genre=LatestTestGenreA&limit=1&offset=1", &resp) + assert.Equal(t, 200, status) + assert.Equal(t, 1, len(resp.Data)) + assert.Equal(t, trashid.MustEncodeHashID(800), resp.Data[0].ID) +} + +func TestGetLatestExcludesUnlisted(t *testing.T) { + app := latestTestApp(t) + var resp struct { + Data []dbv1.Track + } + status, _ := testGet(t, app, "/v1/tracks/latest?genre=LatestTestGenreB", &resp) + assert.Equal(t, 200, status) + assert.Equal(t, 1, len(resp.Data)) + assert.Equal(t, trashid.MustEncodeHashID(803), resp.Data[0].ID) +}