diff --git a/api/v1_events_remix_contests.go b/api/v1_events_remix_contests.go index 508fa081..d1f9ee20 100644 --- a/api/v1_events_remix_contests.go +++ b/api/v1_events_remix_contests.go @@ -18,13 +18,14 @@ type GetRemixContestsParams struct { // v1EventsRemixContests returns remix-contest events from the events table. // Sort priority: -// 1. Open contests whose host is followed by the Audius account. -// 2. Open contests whose host is not followed by the Audius account. -// 3. Ended contests whose host is followed by the Audius account. -// 4. Ended contests whose host is not followed by the Audius account. +// 1. Open contests with a featured host. +// 2. Open contests with any other host. +// 3. Ended contests with a featured host. +// 4. Ended contests with any other host. // -// The Audius account is config.Cfg.FeaturedAudienceUserID; 0 disables the -// follow-based tiebreak (groups collapse to open-vs-ended only). +// A featured host is verified, is the Audius account, or is followed by the +// Audius account. The Audius account is config.Cfg.FeaturedAudienceUserID; +// 0 leaves verified as the only signal. // Within each group, results are sorted by entry count descending, then by // the active-first / soonest-ending sort, then event_id ASC as a stable tiebreak. // Supports pagination and an optional `status` filter (active | ended | all). @@ -132,9 +133,10 @@ func (app *ApiServer) v1EventsRemixContests(c *fiber.Ctx) error { ORDER BY -- Primary: open (0) before ended (1). CASE WHEN e.end_date IS NULL OR e.end_date > NOW() THEN 0 ELSE 1 END ASC, - -- Secondary: Audius-followed hosts (0) before unfollowed (1). - -- When @audius_user_id is 0 the tiebreak collapses (all rows score 1). + -- Secondary: featured hosts (0) before the rest (1). CASE + WHEN u.is_verified THEN 0 + WHEN @audius_user_id::int4 != 0 AND e.user_id = @audius_user_id::int4 THEN 0 WHEN @audius_user_id::int4 != 0 AND EXISTS ( SELECT 1 FROM follows f diff --git a/api/v1_events_remix_contests_test.go b/api/v1_events_remix_contests_test.go index f8f2d97c..a380f5d6 100644 --- a/api/v1_events_remix_contests_test.go +++ b/api/v1_events_remix_contests_test.go @@ -490,6 +490,78 @@ func TestRemixContestsFollowFilterIgnoresStaleRows(t *testing.T) { }) } +// TestRemixContestsFeaturesAudiusAndVerifiedHosts: a contest hosted by the +// Audius account or by a verified host sorts ahead of other open contests, +// even with fewer entries. +func TestRemixContestsFeaturesAudiusAndVerifiedHosts(t *testing.T) { + app := emptyTestApp(t) + + audiusUserID := 9300 + verifiedHostID := 9301 + plainHostID := 9302 + remixerID := 9303 + + audiusTrackID := 8601 + verifiedTrackID := 8602 + plainTrackID := 8603 + + start := parseTime(t, "2024-01-02") + inWindow := parseTime(t, "2024-01-03") + end := parseTime(t, "2099-01-01") + + fixtures := database.FixtureMap{ + "events": []map[string]any{ + { + "event_id": 801, "event_type": "remix_contest", "entity_type": "track", + "entity_id": plainTrackID, "user_id": plainHostID, + "created_at": start, "end_date": end, + }, + { + "event_id": 802, "event_type": "remix_contest", "entity_type": "track", + "entity_id": verifiedTrackID, "user_id": verifiedHostID, + "created_at": start, "end_date": end, + }, + { + "event_id": 803, "event_type": "remix_contest", "entity_type": "track", + "entity_id": audiusTrackID, "user_id": audiusUserID, + "created_at": start, "end_date": end, + }, + }, + "users": []map[string]any{ + {"user_id": audiusUserID, "handle": "audius"}, + {"user_id": verifiedHostID, "handle": "verified_host", "is_verified": true}, + {"user_id": plainHostID, "handle": "plain_host"}, + {"user_id": remixerID, "handle": "remixer"}, + }, + "tracks": []map[string]any{ + {"track_id": audiusTrackID, "owner_id": audiusUserID, "created_at": start}, + {"track_id": verifiedTrackID, "owner_id": verifiedHostID, "created_at": start}, + {"track_id": plainTrackID, "owner_id": plainHostID, "created_at": start}, + {"track_id": 8611, "owner_id": remixerID, "created_at": inWindow}, + {"track_id": 8612, "owner_id": remixerID, "created_at": inWindow}, + }, + "remixes": []map[string]any{ + {"parent_track_id": plainTrackID, "child_track_id": 8611}, + {"parent_track_id": plainTrackID, "child_track_id": 8612}, + }, + } + database.Seed(app.pool.Replicas[0], fixtures) + + prev := config.Cfg.FeaturedAudienceUserID + config.Cfg.FeaturedAudienceUserID = int32(audiusUserID) + t.Cleanup(func() { config.Cfg.FeaturedAudienceUserID = prev }) + + status, body := testGet(t, app, "/v1/events/remix-contests") + assert.Equal(t, 200, status) + + jsonAssert(t, body, map[string]any{ + "data.#": 3, + "data.0.event_id": trashid.MustEncodeHashID(802), // verified host, 0 entries + "data.1.event_id": trashid.MustEncodeHashID(803), // Audius account, 0 entries + "data.2.event_id": trashid.MustEncodeHashID(801), // 2 entries + }) +} + // TestRemixContestsExcludesUnavailableContent covers server-side filtering // of contests whose track or host is not in a publishable state. The // frontend used to drop these on the client (the "deleted accounts surface diff --git a/config/config.go b/config/config.go index 4a4a5d95..38e48a5e 100644 --- a/config/config.go +++ b/config/config.go @@ -75,10 +75,10 @@ type Config struct { AudiusApiSecret string // Shared secret for notifications-dashboard (or other internal jobs) to read notification campaign push open counts NotificationCampaignOpenMetricsSecret string - // Audius account user id. In the public remix-contests list, hosts followed - // by this account sort ahead of other hosts within both the open and ended - // groups. Zero (the default when the env var is unset) disables follow-based - // prioritization (the list reduces to open-before-ended). + // Audius account user id. In the public remix-contests list, this account + // and the hosts it follows sort ahead of other hosts within both the open + // and ended groups. Defaults to the Audius account in prod; zero elsewhere + // unless featuredAudienceUserId is set. FeaturedAudienceUserID int32 // Genesis migration dual-write queue. @@ -270,6 +270,7 @@ func init() { case "prod": fallthrough case "production": + Cfg.FeaturedAudienceUserID = 51 Cfg.OpenAudioURLs = []string{ "rpc.audius.co", }