From e1a79d9da52f8622c3282aab42c80084550da02c Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Wed, 23 Apr 2025 13:59:55 -0700 Subject: [PATCH] [API-87] Add trending ids route --- api/server.go | 1 + api/testdata/track_fixtures.csv | 1 + .../track_trending_scores_fixtures.csv | 3 +- api/testdata/user_fixtures.csv | 1 + api/v1_tracks_trending.go | 38 +---- api/v1_tracks_trending_ids.go | 137 ++++++++++++++++++ api/v1_tracks_trending_ids_test.go | 28 ++++ 7 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 api/v1_tracks_trending_ids.go create mode 100644 api/v1_tracks_trending_ids_test.go diff --git a/api/server.go b/api/server.go index 560415fd..f176e9eb 100644 --- a/api/server.go +++ b/api/server.go @@ -200,6 +200,7 @@ func NewApiServer(config config.Config) *ApiServer { g.Get("/tracks", app.v1Tracks) g.Get("/tracks/trending", app.v1TracksTrending) + g.Get("/tracks/trending/ids", app.v1TracksTrendingIds) g.Get("/tracks/recommended", app.v1TracksTrending) g.Use("/tracks/:trackId", app.requireTrackIdMiddleware) diff --git a/api/testdata/track_fixtures.csv b/api/testdata/track_fixtures.csv index be33c716..e1d47e3a 100644 --- a/api/testdata/track_fixtures.csv +++ b/api/testdata/track_fixtures.csv @@ -7,3 +7,4 @@ track_id,genre,owner_id,title,is_unlisted,stream_conditions,download_conditions 300,Electronic,3,Follow Gated Download,f,,"{""follow_user_id"": 3}" 301,Electronic,3,Pay Gated Download,f,,"{""usdc_purchase"": {""price"": 135, ""splits"": [{""user_id"": 3, ""percentage"": 100.0}]}}" 302,Electronic,3,Tip Gated Stream,f," {""tip_user_id"": 3}"," {""tip_user_id"": 3}" +400,Folk,5,Trending Month Folk,f,, diff --git a/api/testdata/track_trending_scores_fixtures.csv b/api/testdata/track_trending_scores_fixtures.csv index 439d99b0..f90b7039 100644 --- a/api/testdata/track_trending_scores_fixtures.csv +++ b/api/testdata/track_trending_scores_fixtures.csv @@ -4,4 +4,5 @@ track_id,genre,time_range,score 201,Alternative,week,2.0 202,Alternative,week,2.0 300,Electronic,week,3.0 -300,Electronic,allTime,3.0 \ No newline at end of file +300,Electronic,allTime,3.0 +400,Folk,month,10.0 diff --git a/api/testdata/user_fixtures.csv b/api/testdata/user_fixtures.csv index 1036a24c..7cf2894d 100644 --- a/api/testdata/user_fixtures.csv +++ b/api/testdata/user_fixtures.csv @@ -3,4 +3,5 @@ user_id,handle,handle_lc,is_deactivated,wallet,playlist_library 2,stereosteve,stereosteve,f,0x1234567890abcdef, 3,someseller,someseller,f,0x234567890abcdef1, 4,accesstester,accesstester,f,0x34567890abcdef12, +5,guyintrending,guyintrending,f,0x34567890abcdef13, 91,badguy,badguy,t,0x4567890abcdef123, diff --git a/api/v1_tracks_trending.go b/api/v1_tracks_trending.go index a4e5d5ab..b0417f95 100644 --- a/api/v1_tracks_trending.go +++ b/api/v1_tracks_trending.go @@ -3,43 +3,19 @@ package api import ( "bridgerton.audius.co/api/dbv1" "github.com/gofiber/fiber/v2" - "github.com/jackc/pgx/v5" ) func (app *ApiServer) v1TracksTrending(c *fiber.Ctx) error { myId := app.getMyId(c) - sql := ` - SELECT track_trending_scores.track_id - FROM track_trending_scores - LEFT JOIN tracks - ON tracks.track_id = track_trending_scores.track_id - AND tracks.is_delete = false - AND tracks.is_unlisted = false - AND tracks.is_available = true - WHERE type = 'TRACKS' - AND version = 'pnagD' - AND time_range = @time - AND (@genre = '' OR track_trending_scores.genre = @genre) - ORDER BY - score DESC, - track_id DESC - LIMIT @limit - OFFSET @offset - ` + trackIds, err := app.getTrendingIds( + c, + c.Query("time", "week"), + c.Query("genre", ""), + c.QueryInt("limit", 100), + c.QueryInt("offset", 0), + ) - args := pgx.NamedArgs{} - args["limit"] = c.Query("limit", "100") - args["offset"] = c.Query("offset", "0") - args["time"] = c.Query("time", "week") - args["genre"] = c.Query("genre", "") - - rows, err := app.pool.Query(c.Context(), sql, args) - if err != nil { - return err - } - - trackIds, err := pgx.CollectRows(rows, pgx.RowTo[int32]) if err != nil { return err } diff --git a/api/v1_tracks_trending_ids.go b/api/v1_tracks_trending_ids.go new file mode 100644 index 00000000..be06a903 --- /dev/null +++ b/api/v1_tracks_trending_ids.go @@ -0,0 +1,137 @@ +package api + +import ( + "bridgerton.audius.co/trashid" + "github.com/gofiber/fiber/v2" + "github.com/jackc/pgx/v5" +) + +func (app *ApiServer) getTrendingIds(c *fiber.Ctx, timeRange string, genre string, limit int, offset int) ([]int32, error) { + sql := ` + SELECT track_trending_scores.track_id + FROM track_trending_scores + LEFT JOIN tracks + ON tracks.track_id = track_trending_scores.track_id + AND tracks.is_delete = false + AND tracks.is_unlisted = false + AND tracks.is_available = true + WHERE type = 'TRACKS' + AND version = 'pnagD' + AND time_range = @time + AND (@genre = '' OR track_trending_scores.genre = @genre) + ORDER BY + score DESC, + track_id DESC + LIMIT @limit + OFFSET @offset + ` + + args := pgx.NamedArgs{} + args["limit"] = limit + args["offset"] = offset + args["time"] = timeRange + args["genre"] = genre + + rows, err := app.pool.Query(c.Context(), sql, args) + if err != nil { + return nil, err + } + + trackIds, err := pgx.CollectRows(rows, pgx.RowTo[int32]) + if err != nil { + return nil, err + } + + return trackIds, nil +} + +type hashIdResponse struct { + ID string `json:"id"` +} + +func encodeIds(ids []int32) ([]hashIdResponse, error) { + result := make([]hashIdResponse, len(ids)) + for i, id := range ids { + encoded, err := trashid.EncodeHashId(int(id)) + if err != nil { + return nil, err + } + result[i] = hashIdResponse{ID: encoded} + } + return result, nil +} + +func (app *ApiServer) v1TracksTrendingIds(c *fiber.Ctx) error { + limit := c.QueryInt("limit", 100) + offset := c.QueryInt("offset", 0) + genre := "" + + weekChan := make(chan []int32) + monthChan := make(chan []int32) + yearChan := make(chan []int32) + errChan := make(chan error) + + go func() { + ids, err := app.getTrendingIds(c, "week", genre, limit, offset) + if err != nil { + errChan <- err + return + } + weekChan <- ids + }() + + go func() { + ids, err := app.getTrendingIds(c, "month", genre, limit, offset) + if err != nil { + errChan <- err + return + } + monthChan <- ids + }() + + go func() { + ids, err := app.getTrendingIds(c, "allTime", genre, limit, offset) + if err != nil { + errChan <- err + return + } + yearChan <- ids + }() + + var weekIds, monthIds, yearIds []int32 + + for i := 0; i < 3; i++ { + select { + case weekIds = <-weekChan: + case monthIds = <-monthChan: + case yearIds = <-yearChan: + case err := <-errChan: + return err + } + } + + weekHashedIds, err := encodeIds(weekIds) + if err != nil { + return err + } + + monthHashedIds, err := encodeIds(monthIds) + if err != nil { + return err + } + + yearHashedIds, err := encodeIds(yearIds) + if err != nil { + return err + } + + return c.JSON(fiber.Map{ + "data": fiber.Map{ + "week": weekHashedIds, + "month": monthHashedIds, + // Note that this is technically all time, but is set as + // year for backwards compatibility + "year": yearHashedIds, + }, + }) +} diff --git a/api/v1_tracks_trending_ids_test.go b/api/v1_tracks_trending_ids_test.go new file mode 100644 index 00000000..ca6da05d --- /dev/null +++ b/api/v1_tracks_trending_ids_test.go @@ -0,0 +1,28 @@ +package api + +import ( + "testing" + + "bridgerton.audius.co/trashid" + "github.com/stretchr/testify/assert" +) + +func TestGetTrendingIds(t *testing.T) { + var resp struct { + Data struct { + Week []hashIdResponse `json:"week"` + Month []hashIdResponse `json:"month"` + Year []hashIdResponse `json:"year"` + } `json:"data"` + } + status, _ := testGet(t, "/v1/tracks/trending/ids", &resp) + assert.Equal(t, 200, status) + + assert.Equal(t, trashid.MustEncodeHashID(300), resp.Data.Week[0].ID) + assert.Equal(t, trashid.MustEncodeHashID(202), resp.Data.Week[1].ID) + assert.Equal(t, trashid.MustEncodeHashID(201), resp.Data.Week[2].ID) + assert.Equal(t, trashid.MustEncodeHashID(200), resp.Data.Week[3].ID) + assert.Equal(t, trashid.MustEncodeHashID(400), resp.Data.Month[0].ID) + assert.Equal(t, trashid.MustEncodeHashID(200), resp.Data.Year[0].ID) + assert.Equal(t, trashid.MustEncodeHashID(300), resp.Data.Year[1].ID) +}