From 30e36c20c7d843b67bf68376dcc65e1dc3e4bffe Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Wed, 18 Mar 2026 12:43:20 -0700 Subject: [PATCH] Add total track downloads --- ...get_user_track_download_count_total.sql.go | 37 +++++++++++++++++++ .../get_user_track_download_count_total.sql | 18 +++++++++ api/server.go | 1 + api/swagger/swagger-v1.yaml | 36 ++++++++++++++++++ api/v1_track_download_count_test.go | 24 ++++++++++++ api/v1_users_tracks_download_count.go | 23 ++++++++++++ config/config.go | 2 +- 7 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 api/dbv1/get_user_track_download_count_total.sql.go create mode 100644 api/dbv1/queries/get_user_track_download_count_total.sql create mode 100644 api/v1_users_tracks_download_count.go diff --git a/api/dbv1/get_user_track_download_count_total.sql.go b/api/dbv1/get_user_track_download_count_total.sql.go new file mode 100644 index 00000000..0581999c --- /dev/null +++ b/api/dbv1/get_user_track_download_count_total.sql.go @@ -0,0 +1,37 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: get_user_track_download_count_total.sql + +package dbv1 + +import ( + "context" +) + +const getUserTrackDownloadCountTotal = `-- name: GetUserTrackDownloadCountTotal :one +SELECT count(*)::bigint AS total_download_count +FROM track_downloads d +WHERE EXISTS ( + SELECT 1 + FROM tracks t + WHERE t.owner_id = $1 + AND t.is_current = true + AND t.is_delete = false + AND ( + (t.stem_of IS NULL AND d.parent_track_id = t.track_id) + OR (t.stem_of IS NOT NULL + AND (t.stem_of->>'parent_track_id')::int = d.parent_track_id + AND d.track_id = t.track_id) + ) +) +` + +// Total download count for all tracks (and stems) owned by the user. +// Each download event is counted once. +func (q *Queries) GetUserTrackDownloadCountTotal(ctx context.Context, userID int32) (int64, error) { + row := q.db.QueryRow(ctx, getUserTrackDownloadCountTotal, userID) + var total_download_count int64 + err := row.Scan(&total_download_count) + return total_download_count, err +} diff --git a/api/dbv1/queries/get_user_track_download_count_total.sql b/api/dbv1/queries/get_user_track_download_count_total.sql new file mode 100644 index 00000000..8c2d2447 --- /dev/null +++ b/api/dbv1/queries/get_user_track_download_count_total.sql @@ -0,0 +1,18 @@ +-- name: GetUserTrackDownloadCountTotal :one +-- Total download count for all tracks (and stems) owned by the user. +-- Each download event is counted once. +SELECT count(*)::bigint AS total_download_count +FROM track_downloads d +WHERE EXISTS ( + SELECT 1 + FROM tracks t + WHERE t.owner_id = @user_id + AND t.is_current = true + AND t.is_delete = false + AND ( + (t.stem_of IS NULL AND d.parent_track_id = t.track_id) + OR (t.stem_of IS NOT NULL + AND (t.stem_of->>'parent_track_id')::int = d.parent_track_id + AND d.track_id = t.track_id) + ) +); diff --git a/api/server.go b/api/server.go index 00c6100e..8d80c11a 100644 --- a/api/server.go +++ b/api/server.go @@ -422,6 +422,7 @@ func NewApiServer(config config.Config) *ApiServer { g.Get("/users/:userId/tags", app.v1UsersTags) g.Get("/users/:userId/tracks", app.v1UserTracks) g.Get("/users/:userId/tracks/count", app.v1UserTracksCount) + g.Get("/users/:userId/tracks/download_count", app.v1UserTracksDownloadCount) g.Get("/users/:userId/tracks/remixed", app.v1UserTracksRemixed) g.Get("/users/:userId/albums", app.v1UserAlbums) g.Get("/users/:userId/playlists", app.v1UserPlaylists) diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index 85a8625f..b205deb5 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -6726,6 +6726,33 @@ paths: "500": description: Server error content: {} + /users/{id}/tracks/download_count: + get: + tags: + - users + description: Gets the total download count for all tracks (and stems) owned by the user. Use for dashboard "Downloads" tile. + operationId: Get User Tracks Download Count + security: + - {} + - OAuth2: + - read + parameters: + - name: id + in: path + description: A User ID + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/user_tracks_download_count_response" + "500": + description: Server error + content: {} /users/{id}/tracks/remixed: get: tags: @@ -11161,6 +11188,15 @@ components: download_count: type: integer description: Full track + all stems (parent) or stem-only (stem) + user_tracks_download_count_response: + type: object + required: + - data + properties: + data: + type: integer + format: int64 + description: Total download count for all tracks (and stems) owned by the user create_user_response: type: object properties: diff --git a/api/v1_track_download_count_test.go b/api/v1_track_download_count_test.go index 7d1e8627..774b154b 100644 --- a/api/v1_track_download_count_test.go +++ b/api/v1_track_download_count_test.go @@ -2,8 +2,11 @@ package api import ( "context" + "fmt" "testing" + "api.audius.co/trashid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -55,3 +58,24 @@ func TestV1TracksDownloadCounts(t *testing.T) { assert.Equal(t, int64(2), byID["eYJyn"], "eYJyn download_count") assert.Equal(t, int64(0), byID["eYZmn"], "eYZmn download_count") } + +func TestV1UserTracksDownloadCount(t *testing.T) { + app := testAppWithFixtures(t) + ctx := context.Background() + require.NotNil(t, app.writePool, "test requires write pool") + + // Track 200 (eYJyn) is owned by user 2. Insert two download rows. + _, err := app.writePool.Exec(ctx, ` + INSERT INTO track_downloads (txhash, blocknumber, parent_track_id, track_id, user_id) + VALUES ('tx-user-total-1', 101, 200, 200, 1), ('tx-user-total-2', 101, 200, 200, 2) + `) + require.NoError(t, err) + + user2Hash := trashid.MustEncodeHashID(2) + url := fmt.Sprintf("/v1/full/users/%s/tracks/download_count", user2Hash) + status, body := testGet(t, app, url, nil) + assert.Equal(t, 200, status) + jsonAssert(t, body, map[string]any{ + "data": 2, + }) +} diff --git a/api/v1_users_tracks_download_count.go b/api/v1_users_tracks_download_count.go new file mode 100644 index 00000000..2e720512 --- /dev/null +++ b/api/v1_users_tracks_download_count.go @@ -0,0 +1,23 @@ +package api + +import ( + "github.com/gofiber/fiber/v2" +) + +// v1UserTracksDownloadCount returns the total download count for all tracks +// (and stems) owned by the user. Use this for dashboard "Downloads" tile +// instead of summing per-track counts client-side. +// +// GET /v1/full/users/:userId/tracks/download_count +func (app *ApiServer) v1UserTracksDownloadCount(c *fiber.Ctx) error { + userId := app.getUserId(c) + + total, err := app.queries.GetUserTrackDownloadCountTotal(c.Context(), userId) + if err != nil { + return err + } + + return c.JSON(fiber.Map{ + "data": total, + }) +} diff --git a/config/config.go b/config/config.go index fcb9905c..710e5211 100644 --- a/config/config.go +++ b/config/config.go @@ -109,7 +109,7 @@ func init() { Cfg.DelegatePrivateKey = "13422b9affd75ff80f94f1ea394e6a6097830cb58cda2d3542f37464ecaee7df" } Cfg.AntiAbuseOracles = []string{"http://audius-discovery-provider-1"} - Cfg.ArchiverNodes = []string{"http://audius-discovery-provider-1"} + Cfg.ArchiverNodes = []string{"https://archiver.audius.engineering"} Cfg.Rewards = core_config.MakeRewards(core_config.DevClaimAuthorities, core_config.DevRewardExtensions) Cfg.AudiusdURL = "https://node1.oap.devnet" Cfg.ChainId = "openaudio-devnet"