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/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/handle/:handle/tracks", app.v1UserTracks)
g.Get("/users/handle/:handle/tracks/count", app.v1UserTracksCount)
g.Get("/users/handle/:handle/albums", app.v1UserAlbums)
g.Get("/users/handle/:handle/contests", app.v1UserContests)
g.Get("/users/handle/:handle/playlists", app.v1UserPlaylists)
g.Get("/users/handle/:handle/tracks/ai_attributed", app.v1UserTracksAiAttributed)
g.Get("/users/handle/:handle/tracks/ai-attributed", app.v1UserTracksAiAttributed)
Expand Down Expand Up @@ -424,6 +425,7 @@ func NewApiServer(config config.Config) *ApiServer {
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/contests", app.v1UserContests)
g.Get("/users/:userId/playlists", app.v1UserPlaylists)
g.Get("/users/:userId/feed", app.v1UsersFeed)
g.Get("/users/:userId/connected_wallets", app.v1UsersConnectedWallets)
Expand Down
58 changes: 58 additions & 0 deletions api/swagger/swagger-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5535,6 +5535,64 @@ paths:
"500":
description: Server error
content: {}
/users/{id}/contests:
get:
tags:
- users
summary: Get contests hosted by user
description:
Get the remix contests hosted by a single user, ordered with
currently-active contests first (by soonest-ending end_date)
followed by ended contests (most-recently-ended first). Mirrors
the response shape of `GET /events/remix-contests` (data +
related users / tracks / entry_counts).
operationId: Get Contests By User
security:
- {}
- OAuth2:
- read
parameters:
- name: id
in: path
description: A User ID
required: true
schema:
type: string
- 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: status
in: query
description: Filter contests by status
schema:
type: string
default: all
enum:
- active
- ended
- all
responses:
"200":
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/remix_contests_response"
"400":
description: Bad request
content: {}
"500":
description: Server error
content: {}
/users/{id}/connected_wallets:
get:
tags:
Expand Down
204 changes: 204 additions & 0 deletions api/v1_users_contests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package api

import (
"strings"

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

type GetUserContestsParams struct {
Limit int `query:"limit" default:"25" validate:"min=1,max=100"`
Offset int `query:"offset" default:"0" validate:"min=0"`
Status string `query:"status" default:"all" validate:"oneof=active ended all"`
}

// v1UserContests returns the remix contests hosted by a single user, in the
// same shape as GET /events/remix-contests (data + related users / tracks /
// entry_counts). Active contests come first (soonest-ending end_date),
// followed by ended contests (most-recently-ended first).
func (app *ApiServer) v1UserContests(c *fiber.Ctx) error {
params := GetUserContestsParams{}
if err := app.ParseAndValidateQueryParams(c, &params); err != nil {
return err
}

userID := app.getUserId(c)

filters := []string{
"e.event_type = 'remix_contest'",
"e.is_deleted = false",
"e.user_id = @user_id",
"(e.entity_type != 'track' OR (t.track_id IS NOT NULL AND t.is_delete = false))",
}

switch params.Status {
case "active":
filters = append(filters, "(e.end_date IS NULL OR e.end_date > NOW())")
case "ended":
filters = append(filters, "(e.end_date IS NOT NULL AND e.end_date <= NOW())")
}

sql := `
SELECT
e.event_id,
e.entity_type::event_entity_type AS entity_type,
e.user_id,
e.entity_id,
e.event_type::event_type AS event_type,
e.end_date,
e.is_deleted,
e.created_at,
e.updated_at,
e.event_data
FROM events e
LEFT JOIN tracks t ON t.track_id = e.entity_id
AND t.is_current = true
AND e.entity_type = 'track'
AND t.access_authorities IS NULL
WHERE ` + strings.Join(filters, " AND ") + `
ORDER BY
CASE WHEN e.end_date IS NULL OR e.end_date > NOW() THEN 0 ELSE 1 END ASC,
CASE WHEN e.end_date IS NULL OR e.end_date > NOW() THEN e.end_date END ASC NULLS LAST,
CASE WHEN e.end_date IS NOT NULL AND e.end_date <= NOW() THEN e.end_date END DESC,
e.event_id ASC
LIMIT @limit OFFSET @offset;
`

rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{
"user_id": userID,
"limit": params.Limit,
"offset": params.Offset,
})
if err != nil {
return err
}
defer rows.Close()

var items []dbv1.GetEventsRow
for rows.Next() {
var row dbv1.GetEventsRow
if err := rows.Scan(
&row.EventID,
&row.EntityType,
&row.UserID,
&row.EntityID,
&row.EventType,
&row.EndDate,
&row.IsDeleted,
&row.CreatedAt,
&row.UpdatedAt,
&row.EventData,
); err != nil {
return err
}
items = append(items, row)
}
if err := rows.Err(); err != nil {
return err
}

data := make([]dbv1.FullEvent, 0, len(items))
trackIDs := make([]int32, 0, len(items))
userIDSet := map[int32]struct{}{}
for _, event := range items {
data = append(data, app.queries.ToFullEvent(event))
if event.EntityType == dbv1.EventEntityTypeTrack && event.EntityID.Valid {
trackIDs = append(trackIDs, event.EntityID.Int32)
}
userIDSet[event.UserID] = struct{}{}
}

myID := app.getMyId(c)
authedWallet := app.tryGetAuthedWallet(c)

var trackMap map[int32]dbv1.Track
if len(trackIDs) > 0 {
trackMap, err = app.queries.TracksKeyed(c.Context(), dbv1.TracksParams{
GetTracksParams: dbv1.GetTracksParams{
Ids: trackIDs,
MyID: myID,
AuthedWallet: authedWallet,
},
})
if err != nil {
return err
}
}
for _, t := range trackMap {
userIDSet[t.GetTracksRow.UserID] = struct{}{}
}

userIDs := make([]int32, 0, len(userIDSet))
for id := range userIDSet {
userIDs = append(userIDs, id)
}
var userMap map[int32]dbv1.User
if len(userIDs) > 0 {
userMap, err = app.queries.UsersKeyed(c.Context(), dbv1.GetUsersParams{
Ids: userIDs,
MyID: myID,
})
if err != nil {
return err
}
}

users := make([]dbv1.User, 0, len(userMap))
for _, u := range userMap {
users = append(users, u)
}
tracks := make([]dbv1.Track, 0, len(trackMap))
for _, t := range trackMap {
tracks = append(tracks, t)
}

entryCounts := map[string]int64{}
if len(trackIDs) > 0 {
countRows, err := app.pool.Query(c.Context(), `
SELECT
e.entity_id,
COUNT(DISTINCT t.track_id) FILTER (
WHERE t.is_current = true
AND t.is_delete = false
AND t.is_unlisted = false
AND t.created_at > e.created_at
AND (e.end_date IS NULL OR t.created_at < e.end_date)
) AS entry_count
FROM events e
LEFT JOIN remixes rm ON rm.parent_track_id = e.entity_id
LEFT JOIN tracks t ON t.track_id = rm.child_track_id
WHERE e.event_type = 'remix_contest'
AND e.is_deleted = false
AND e.entity_type = 'track'
AND e.entity_id = ANY(@track_ids)
GROUP BY e.entity_id
`, pgx.NamedArgs{"track_ids": trackIDs})
if err != nil {
return err
}
defer countRows.Close()
for countRows.Next() {
var parentTrackID int32
var count int64
if err := countRows.Scan(&parentTrackID, &count); err != nil {
return err
}
entryCounts[trashid.MustEncodeHashID(int(parentTrackID))] = count
}
if err := countRows.Err(); err != nil {
return err
}
}

return c.JSON(fiber.Map{
"data": data,
"related": fiber.Map{
"users": users,
"tracks": tracks,
"entry_counts": entryCounts,
},
})
}
Loading
Loading