Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion api/dbv1/get_tracks.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 18 additions & 5 deletions api/dbv1/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion api/dbv1/queries/get_tracks.sql
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ SELECT
cover_original_song_title,
cover_original_artist,
is_owned_by_user,
t.access_authorities
t.access_authorities,
t.feedback_requested_at,
(t.feedback_requested_at IS NOT NULL)::bool AS feedback_requested

FROM tracks t
JOIN aggregate_track using (track_id)
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/tracks/unclaimed_id", app.v1TracksUnclaimedId)

g.Get("/tracks/latest", app.v1TracksLatest)
g.Get("/tracks/feedback", app.v1TracksFeedback)
g.Get("/tracks/trending", app.v1TracksTrending)
g.Get("/tracks/trending/ids", app.v1TracksTrendingIds)
g.Get("/tracks/trending/winners", app.v1TracksTrendingWinners)
Expand Down
61 changes: 61 additions & 0 deletions api/swagger/swagger-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3241,6 +3241,52 @@ paths:
"500":
description: Server error
content: {}
/tracks/feedback:
get:
tags:
- tracks
description: Gets tracks whose artists are asking for feedback, newest request first
operationId: Get Feedback 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:
Expand Down Expand Up @@ -11403,6 +11449,10 @@ components:
items:
type: string
description: Wallet addresses that can sign to authorize stream access (programmable distribution). When empty or omitted, the track is public and validator/creator nodes can serve it.
feedback_requested:
type: boolean
nullable: true
description: Whether the artist is asking listeners for feedback on the track
allowed_api_keys:
type: array
nullable: true
Expand Down Expand Up @@ -11641,6 +11691,10 @@ components:
items:
type: string
description: Wallet addresses that can sign to authorize stream access (programmable distribution). When empty or omitted, the track is public and validator/creator nodes can serve it.
feedback_requested:
type: boolean
nullable: true
description: Whether the artist is asking listeners for feedback on the track
allowed_api_keys:
type: array
nullable: true
Expand Down Expand Up @@ -15402,6 +15456,13 @@ components:
items:
type: string
description: Wallet addresses that can sign to authorize stream access (programmable distribution). When empty or omitted, the track is public and validator/creator nodes can serve it.
feedback_requested:
type: boolean
description: Whether the artist is asking listeners for feedback on the track
feedback_requested_at:
type: string
nullable: true
description: When the artist asked for feedback, or null when they are not asking
stream_conditions:
type: object
description: How to unlock stream access to the track
Expand Down
2 changes: 2 additions & 0 deletions api/v1_track.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ type CreateTrackRequest struct {
StreamConditions *AccessConditions `json:"stream_conditions,omitempty" validate:"omitempty"`
IsStreamGated *bool `json:"is_stream_gated,omitempty"`
AccessAuthorities *[]string `json:"access_authorities,omitempty"`
FeedbackRequested *bool `json:"feedback_requested,omitempty"`
IsDownloadGated *bool `json:"is_download_gated,omitempty"`
AiAttributionUserId *trashid.HashId `json:"ai_attribution_user_id,omitempty" validate:"omitempty,min=0"`
AllowedApiKeys *[]string `json:"allowed_api_keys,omitempty"`
Expand Down Expand Up @@ -182,6 +183,7 @@ type UpdateTrackRequest struct {
StreamConditions *AccessConditions `json:"stream_conditions,omitempty" validate:"omitempty"`
IsStreamGated *bool `json:"is_stream_gated,omitempty"`
AccessAuthorities *[]string `json:"access_authorities,omitempty"`
FeedbackRequested *bool `json:"feedback_requested,omitempty"`
IsDownloadGated *bool `json:"is_download_gated,omitempty"`
AiAttributionUserId *trashid.HashId `json:"ai_attribution_user_id,omitempty" validate:"omitempty,min=0"`
AllowedApiKeys *[]string `json:"allowed_api_keys,omitempty"`
Expand Down
69 changes: 69 additions & 0 deletions api/v1_tracks_feedback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package api

import (
"api.audius.co/api/dbv1"
"github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5"
)

type GetFeedbackTracksParams struct {
Limit int `query:"limit" default:"20" validate:"min=1,max=100"`
Offset int `query:"offset" default:"0" validate:"min=0,max=500"`
Genre string `query:"genre" default:""`
}

// v1TracksFeedback lists tracks whose artist asked for feedback, newest
// request first.
func (app *ApiServer) v1TracksFeedback(c *fiber.Ctx) error {
var params GetFeedbackTracksParams
if err := app.ParseAndValidateQueryParams(c, &params); err != nil {
return err
}

sql := `
SELECT t.track_id
FROM tracks t
JOIN users u ON u.user_id = t.owner_id
WHERE t.feedback_requested_at IS NOT NULL
AND t.is_current = true
AND t.is_delete = false
AND t.is_unlisted = false
AND t.is_available = true
AND t.is_stream_gated = false
AND t.access_authorities IS NULL
AND COALESCE(t.comments_disabled, false) = false
AND u.is_deactivated = false
AND u.is_available = true
AND (@genre = '' OR t.genre = @genre)
ORDER BY
t.feedback_requested_at DESC,
t.track_id DESC
LIMIT @limit
OFFSET @offset
`
rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{
"genre": params.Genre,
"limit": params.Limit,
"offset": params.Offset,
})
if err != nil {
return err
}
trackIds, err := pgx.CollectRows(rows, pgx.RowTo[int32])
if err != nil {
return err
}

tracks, err := app.queries.Tracks(c.Context(), dbv1.TracksParams{
GetTracksParams: dbv1.GetTracksParams{
Ids: trackIds,
MyID: app.getMyId(c),
AuthedWallet: app.tryGetAuthedWallet(c),
},
})
if err != nil {
return err
}

return v1TracksResponse(c, tracks)
}
70 changes: 70 additions & 0 deletions api/v1_tracks_feedback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package api

import (
"testing"

"api.audius.co/api/dbv1"
"api.audius.co/database"
"api.audius.co/trashid"
"github.com/stretchr/testify/assert"
)

func feedbackTestApp(t *testing.T) *ApiServer {
app := testAppWithFixtures(t)
database.SeedTable(app.pool.Replicas[0], "users", []map[string]any{
{"user_id": 8900, "handle": "feedbackdeactivated", "is_deactivated": true},
})
database.SeedTable(app.pool.Replicas[0], "tracks", []map[string]any{
{"track_id": 900, "genre": "FeedbackGenreA", "owner_id": 1, "title": "Asked First", "feedback_requested_at": "2026-01-01 00:00:00"},
{"track_id": 901, "genre": "FeedbackGenreA", "owner_id": 1, "title": "Asked Last", "feedback_requested_at": "2026-03-01 00:00:00"},
{"track_id": 902, "genre": "FeedbackGenreB", "owner_id": 1, "title": "Asked Middle", "feedback_requested_at": "2026-02-01 00:00:00"},
{"track_id": 903, "genre": "FeedbackGenreA", "owner_id": 1, "title": "Not Asking"},
{"track_id": 904, "genre": "FeedbackGenreA", "owner_id": 1, "title": "Unlisted", "is_unlisted": "t", "feedback_requested_at": "2026-03-02 00:00:00"},
{"track_id": 905, "genre": "FeedbackGenreA", "owner_id": 1, "title": "Comments Off", "comments_disabled": "t", "feedback_requested_at": "2026-03-03 00:00:00"},
{"track_id": 906, "genre": "FeedbackGenreA", "owner_id": 8900, "title": "Deactivated Owner", "feedback_requested_at": "2026-03-04 00:00:00"},
})
return app
}

func TestGetFeedbackTracks(t *testing.T) {
app := feedbackTestApp(t)
var resp struct {
Data []dbv1.Track
}
status, _ := testGet(t, app, "/v1/tracks/feedback", &resp)
assert.Equal(t, 200, status)

ids := []string{}
for _, track := range resp.Data {
ids = append(ids, track.ID)
assert.True(t, track.FeedbackRequested)
assert.NotNil(t, track.FeedbackRequestedAt)
}
assert.Equal(t, []string{
trashid.MustEncodeHashID(901),
trashid.MustEncodeHashID(902),
trashid.MustEncodeHashID(900),
}, ids)
}

func TestGetFeedbackTracksGenreAndPaging(t *testing.T) {
app := feedbackTestApp(t)
var resp struct {
Data []dbv1.Track
}
status, _ := testGet(t, app, "/v1/tracks/feedback?genre=FeedbackGenreA&limit=1&offset=1", &resp)
assert.Equal(t, 200, status)
assert.Equal(t, 1, len(resp.Data))
assert.Equal(t, trashid.MustEncodeHashID(900), resp.Data[0].ID)
}

func TestTrackFeedbackRequestedField(t *testing.T) {
app := feedbackTestApp(t)
var resp struct {
Data dbv1.Track
}
status, _ := testGet(t, app, "/v1/tracks/"+trashid.MustEncodeHashID(903), &resp)
assert.Equal(t, 200, status)
assert.False(t, resp.Data.FeedbackRequested)
assert.Nil(t, resp.Data.FeedbackRequestedAt)
}
9 changes: 9 additions & 0 deletions ddl/migrations/0243_tracks_feedback_requested_at.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- When the artist turned on "looking for feedback"; NULL when off.
-- The indexer (go-openaudio pkg/etl) writes it from track metadata.
ALTER TABLE tracks ADD COLUMN IF NOT EXISTS feedback_requested_at timestamp without time zone;

-- Serves /v1/tracks/feedback (newest request first). Almost every row is
-- NULL, so the partial index stays tiny.
CREATE INDEX CONCURRENTLY IF NOT EXISTS tracks_feedback_requested_at_idx
ON public.tracks USING btree (feedback_requested_at DESC, track_id DESC)
WHERE feedback_requested_at IS NOT NULL AND is_current = true AND is_delete = false;
7 changes: 7 additions & 0 deletions sql/01_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6614,6 +6614,7 @@ CREATE TABLE public.tracks (
parental_warning public.parental_warning_type,
territory_codes text[],
access_authorities text[],
feedback_requested_at timestamp without time zone,
CONSTRAINT check_territory_codes CHECK (public.validate_territory_codes(territory_codes))
);

Expand Down Expand Up @@ -15430,6 +15431,12 @@ CREATE INDEX tracks_ai_attribution_user_id ON public.tracks USING btree (ai_attr
CREATE INDEX tracks_blocknumber_idx ON public.tracks USING btree (blocknumber);


--
-- Name: tracks_feedback_requested_at_idx; Type: INDEX; Schema: public; Owner: -
--

CREATE INDEX tracks_feedback_requested_at_idx ON public.tracks USING btree (feedback_requested_at DESC, track_id DESC) WHERE ((feedback_requested_at IS NOT NULL) AND (is_current = true) AND (is_delete = false));

--
-- Name: tracks_isrc_normalized_idx; Type: INDEX; Schema: public; Owner: -
--
Expand Down