Skip to content
Merged
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
2 changes: 2 additions & 0 deletions api/swagger/swagger-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11981,6 +11981,8 @@ components:
type: array
items:
$ref: "#/components/schemas/event"
related:
$ref: "#/components/schemas/remix_contests_related"
remix_contests_related:
type: object
properties:
Expand Down
60 changes: 60 additions & 0 deletions api/v1_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"api.audius.co/api/dbv1"
"api.audius.co/trashid"
"github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5"
)

type GetEventsParams struct {
Expand Down Expand Up @@ -54,7 +55,66 @@ func (app *ApiServer) v1Events(c *fiber.Ctx) error {
data = append(data, app.queries.ToFullEvent(event))
}

// Compute per-contest entry counts so consumers that resolve a contest via
// this endpoint (the track-page contest section, cold/deep-linked contest
// pages, web Explore's featured contests) can prime
// useRemixesCount({ isContestEntry: true }) instead of firing a separate
// /tracks/{id}/remixes?only_contest_entries=true&limit=0 per card. Mirrors
// the entry-count filter in v1EventsRemixContests: a child track is an entry
// iff it was created after the contest started, before its end_date, and is
// currently listed. Only remix_contest events on track entities have a
// meaningful entry count.
entryCounts := map[string]int64{}
contestEventIds := []int32{}
for _, event := range recentEvents {
if event.EventType == dbv1.EventTypeRemixContest &&
event.EntityType == dbv1.EventEntityTypeTrack &&
event.EntityID.Valid {
contestEventIds = append(contestEventIds, event.EventID)
// Default to 0 so the UI primes a definitive "no entries" and
// still skips the count-only request for empty contests.
entryCounts[trashid.MustEncodeHashID(int(event.EntityID.Int32))] = 0
}
}

if len(contestEventIds) > 0 {
countSql := `
SELECT e.entity_id, COUNT(DISTINCT ct.track_id) AS entry_count
FROM events e
JOIN remixes rm ON rm.parent_track_id = e.entity_id
JOIN tracks ct ON ct.track_id = rm.child_track_id
WHERE e.event_id = ANY(@event_ids)
AND ct.is_current = true
AND ct.is_delete = false
AND ct.is_unlisted = false
AND ct.created_at > e.created_at
AND (e.end_date IS NULL OR ct.created_at < e.end_date)
GROUP BY e.entity_id;
`
rows, err := app.pool.Query(c.Context(), countSql, pgx.NamedArgs{
"event_ids": contestEventIds,
})
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var entityID int32
var entryCount int64
if err := rows.Scan(&entityID, &entryCount); err != nil {
return err
}
entryCounts[trashid.MustEncodeHashID(int(entityID))] = entryCount
}
if err := rows.Err(); err != nil {
return err
}
}

return c.JSON(fiber.Map{
"data": data,
"related": fiber.Map{
"entry_counts": entryCounts,
},
})
}
84 changes: 84 additions & 0 deletions api/v1_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"api.audius.co/api/dbv1"
"api.audius.co/database"
"api.audius.co/trashid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -87,6 +88,89 @@ func TestGetEventsExcludesDeletedTracks(t *testing.T) {
})
}

// TestGetEntityEventsEntryCounts verifies the /events/entity endpoint returns
// related.entry_counts using the same in-window filter as the remix-contests
// discovery endpoint, so callers can prime useRemixesCount({ isContestEntry:
// true }) instead of issuing a separate /tracks/{id}/remixes?limit=0 per card.
func TestGetEntityEventsEntryCounts(t *testing.T) {
app := emptyTestApp(t)

hostID := 7101
remixer := 7102

contestTrackID := 7001
contestStart := parseTime(t, "2024-01-02")
contestEnd := parseTime(t, "2099-01-01")

inWindow := parseTime(t, "2024-01-03")
tooEarly := parseTime(t, "2024-01-01") // before contest start => excluded

fixtures := database.FixtureMap{
"events": []map[string]any{
{
"event_id": 601,
"event_type": "remix_contest",
"entity_type": "track",
"entity_id": contestTrackID,
"user_id": hostID,
"created_at": contestStart,
"end_date": contestEnd,
},
},
"users": []map[string]any{
{"user_id": hostID, "handle": "entryhost"},
{"user_id": remixer, "handle": "entryremixer"},
},
"tracks": []map[string]any{
{
"track_id": contestTrackID,
"owner_id": hostID,
"title": "Contest Parent",
"created_at": contestStart,
},
{
"track_id": 7201,
"owner_id": remixer,
"title": "In Window Entry A",
"created_at": inWindow,
},
{
"track_id": 7202,
"owner_id": remixer,
"title": "In Window Entry B",
"created_at": inWindow,
},
{
"track_id": 7203,
"owner_id": remixer,
"title": "Too Early (excluded)",
"created_at": tooEarly,
},
},
"remixes": []map[string]any{
{"parent_track_id": contestTrackID, "child_track_id": 7201},
{"parent_track_id": contestTrackID, "child_track_id": 7202},
{"parent_track_id": contestTrackID, "child_track_id": 7203},
},
}
database.Seed(app.pool.Replicas[0], fixtures)

contestTrackHash := trashid.MustEncodeHashID(contestTrackID)

status, body := testGet(
t, app,
"/v1/events/entity?entity_id="+contestTrackHash,
)
assert.Equal(t, 200, status)

// 2 in-window remixes counted; the pre-window remix (7203) excluded.
jsonAssert(t, body, map[string]any{
"data.0.event_id": trashid.MustEncodeHashID(601),
"data.0.entity_id": contestTrackHash,
"related.entry_counts." + contestTrackHash: float64(2),
})
}

func TestGetEventsExcludesAccessAuthoritiesTracks(t *testing.T) {
app := testAppWithFixtures(t)
ctx := context.Background()
Expand Down
Loading