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
54 changes: 54 additions & 0 deletions api/dbv1/collaborator_invites.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
package dbv1

import (
"context"
"time"

"api.audius.co/trashid"
)

// FullTrackCollaboratorInvite is a collaborator credit from the invited user's
// perspective: which track, who invited them, and the current status.
type FullTrackCollaboratorInvite struct {
TrackID string `json:"track_id"`
Status string `json:"status"`
InvitedBy User `json:"invited_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FullTrackCollaboratorInvites resolves a user's collaborator invites, embedding
// the inviter as a full user object (mirrors FullManagers).
func (q *Queries) FullTrackCollaboratorInvites(ctx context.Context, params GetTrackCollaboratorInvitesForUserParams) ([]FullTrackCollaboratorInvite, error) {
rows, err := q.GetTrackCollaboratorInvitesForUser(ctx, params)
if err != nil {
return nil, err
}

inviterIds := make([]int32, len(rows))
for i, row := range rows {
inviterIds[i] = row.InvitedBy
}

users, err := q.UsersKeyed(ctx, GetUsersParams{
Ids: inviterIds,
MyID: params.UserID,
})
if err != nil {
return nil, err
}

invites := make([]FullTrackCollaboratorInvite, len(rows))
for i, row := range rows {
trackID, _ := trashid.EncodeHashId(int(row.TrackID))
invites[i] = FullTrackCollaboratorInvite{
TrackID: trackID,
Status: row.Status,
InvitedBy: users[row.InvitedBy],
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}

return invites, nil
}
24 changes: 21 additions & 3 deletions api/dbv1/models.go

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

20 changes: 20 additions & 0 deletions api/dbv1/queries/track_collaborators.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
-- Accepted collaborators for a set of tracks, used to embed a `collaborators`
-- array on track responses. Returns one row per (track, collaborator); the Go
-- layer bulk-resolves the user objects. Backed by the track_collaborators
-- primary key (track_id leads), so the ANY(...) lookup is index-served.
-- name: GetTrackCollaborators :many
SELECT track_id, collaborator_user_id
FROM track_collaborators
WHERE track_id = ANY(@track_ids::int[])
AND status = 'accepted'
ORDER BY track_id, created_at;

-- A user's collaborator invites/credits, optionally filtered by status
-- (pending/accepted/rejected). Backed by the covering
-- (collaborator_user_id, status, track_id) index.
-- name: GetTrackCollaboratorInvitesForUser :many
SELECT track_id, collaborator_user_id, invited_by, status, created_at, updated_at
FROM track_collaborators
WHERE collaborator_user_id = @user_id::int
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY created_at DESC;
102 changes: 102 additions & 0 deletions api/dbv1/track_collaborators.sql.go

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

42 changes: 33 additions & 9 deletions api/dbv1/tracks.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,16 @@ const IncludeID3TagsCtxKey = "includeID3Tags"
type Track struct {
GetTracksRow

Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Access Access `json:"access"`
Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Collaborators []User `json:"collaborators"`
Access Access `json:"access"`

FolloweeReposts []*FolloweeRepost `json:"followee_reposts"`
FolloweeFavorites []*FolloweeFavorite `json:"followee_favorites"`
Expand All@@ -45,6 +46,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

userIds := []int32{}
trackIds := make([]int32, 0, len(rawTracks))
collectSplitUserIds := func(usage *AccessGate) {
if usage == nil || usage.UsdcPurchase == nil {
return
Expand All@@ -56,6 +58,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

for _, rawTrack := range rawTracks {
userIds = append(userIds, rawTrack.UserID)
trackIds = append(trackIds, rawTrack.TrackID)

var remixOf RemixOf
json.Unmarshal(rawTrack.RemixOf, &remixOf)
Expand All@@ -67,6 +70,18 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
collectSplitUserIds(rawTrack.DownloadConditions)
}

// Fetch accepted collaborators for these tracks in one query, and fold
// their user IDs into the bulk user fetch below so each is fully resolved.
collaboratorRows, err := q.GetTrackCollaborators(ctx, trackIds)
if err != nil {
return nil, err
}
collaboratorsByTrack := map[int32][]int32{}
for _, cr := range collaboratorRows {
collaboratorsByTrack[cr.TrackID] = append(collaboratorsByTrack[cr.TrackID], cr.CollaboratorUserID)
userIds = append(userIds, cr.CollaboratorUserID)
}

userMap, err := q.UsersKeyed(ctx, GetUsersParams{
MyID: arg.MyID.(int32),
Ids: userIds,
Expand DownExpand Up@@ -130,6 +145,14 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// Resolve accepted collaborators (order preserved from the query).
collaborators := []User{}
for _, cid := range collaboratorsByTrack[rawTrack.TrackID] {
if cu, ok := userMap[cid]; ok {
collaborators = append(collaborators, cu)
}
}

// Get access from the bulk access map
access := accessMap[rawTrack.TrackID]

Expand DownExpand Up@@ -179,6 +202,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
Preview: preview,
User: user,
UserID: user.ID,
Collaborators: collaborators,
FolloweeFavorites: fullFolloweeFavorites(rawTrack.FolloweeFavorites),
FolloweeReposts: fullFolloweeReposts(rawTrack.FolloweeReposts),
RemixOf: fullRemixOf,
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -432,6 +432,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/:userId/balance/history", app.v1UsersBalanceHistory)
g.Get("/users/:userId/managers", app.v1UsersManagers)
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
g.Get("/users/:userId/collaboration_invites", app.v1UsersCollaborationInvites)
g.Get("/grantees/:address/users", app.v1GranteeUsers)
g.Post("/users/:userId/grants", app.requireAuthMiddleware, app.requireWriteScope, app.postV1UsersGrant)
g.Delete("/users/:userId/grants/:address", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1UsersGrant)
Expand Down
4 changes: 3 additions & 1 deletion api/v1_notifications.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const notificationRelatedActorsPerGroup = 1
type GetNotificationsQueryParams struct {
// Note that when limit is 0, we return 20 items to calculate unread count
Limit int `query:"limit" default:"20" validate:"min=0,max=100"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request track_collaborator_invite track_collaborator_accept claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
GroupID string `query:"group_id" validate:"omitempty"`
Timestamp float64 `query:"timestamp" validate:"omitempty,min=0"`
}
Expand DownExpand Up@@ -378,6 +378,8 @@ func collectNotificationRelatedIds(action json.RawMessage, userIds, trackIds, pl
"data.receiver_user_id",
"data.dethroned_user_id",
"data.grantee_user_id",
"data.inviter_user_id",
"data.collaborator_user_id",
"data.tastemaker_user_id",
"data.tastemaker_item_owner_id",
"data.track_owner_id",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
54 changes: 54 additions & 0 deletions api/dbv1/collaborator_invites.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
package dbv1

import (
"context"
"time"

"api.audius.co/trashid"
)

// FullTrackCollaboratorInvite is a collaborator credit from the invited user's
// perspective: which track, who invited them, and the current status.
type FullTrackCollaboratorInvite struct {
TrackID string `json:"track_id"`
Status string `json:"status"`
InvitedBy User `json:"invited_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FullTrackCollaboratorInvites resolves a user's collaborator invites, embedding
// the inviter as a full user object (mirrors FullManagers).
func (q *Queries) FullTrackCollaboratorInvites(ctx context.Context, params GetTrackCollaboratorInvitesForUserParams) ([]FullTrackCollaboratorInvite, error) {
rows, err := q.GetTrackCollaboratorInvitesForUser(ctx, params)
if err != nil {
return nil, err
}

inviterIds := make([]int32, len(rows))
for i, row := range rows {
inviterIds[i] = row.InvitedBy
}

users, err := q.UsersKeyed(ctx, GetUsersParams{
Ids: inviterIds,
MyID: params.UserID,
})
if err != nil {
return nil, err
}

invites := make([]FullTrackCollaboratorInvite, len(rows))
for i, row := range rows {
trackID, _ := trashid.EncodeHashId(int(row.TrackID))
invites[i] = FullTrackCollaboratorInvite{
TrackID: trackID,
Status: row.Status,
InvitedBy: users[row.InvitedBy],
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}

return invites, nil
}
24 changes: 21 additions & 3 deletions api/dbv1/models.go

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

20 changes: 20 additions & 0 deletions api/dbv1/queries/track_collaborators.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
-- Accepted collaborators for a set of tracks, used to embed a `collaborators`
-- array on track responses. Returns one row per (track, collaborator); the Go
-- layer bulk-resolves the user objects. Backed by the track_collaborators
-- primary key (track_id leads), so the ANY(...) lookup is index-served.
-- name: GetTrackCollaborators :many
SELECT track_id, collaborator_user_id
FROM track_collaborators
WHERE track_id = ANY(@track_ids::int[])
AND status = 'accepted'
ORDER BY track_id, created_at;

-- A user's collaborator invites/credits, optionally filtered by status
-- (pending/accepted/rejected). Backed by the covering
-- (collaborator_user_id, status, track_id) index.
-- name: GetTrackCollaboratorInvitesForUser :many
SELECT track_id, collaborator_user_id, invited_by, status, created_at, updated_at
FROM track_collaborators
WHERE collaborator_user_id = @user_id::int
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY created_at DESC;
102 changes: 102 additions & 0 deletions api/dbv1/track_collaborators.sql.go

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

42 changes: 33 additions & 9 deletions api/dbv1/tracks.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,16 @@ const IncludeID3TagsCtxKey = "includeID3Tags"
type Track struct {
GetTracksRow

Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Access Access `json:"access"`
Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Collaborators []User `json:"collaborators"`
Access Access `json:"access"`

FolloweeReposts []*FolloweeRepost `json:"followee_reposts"`
FolloweeFavorites []*FolloweeFavorite `json:"followee_favorites"`
Expand All@@ -45,6 +46,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

userIds := []int32{}
trackIds := make([]int32, 0, len(rawTracks))
collectSplitUserIds := func(usage *AccessGate) {
if usage == nil || usage.UsdcPurchase == nil {
return
Expand All@@ -56,6 +58,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

for _, rawTrack := range rawTracks {
userIds = append(userIds, rawTrack.UserID)
trackIds = append(trackIds, rawTrack.TrackID)

var remixOf RemixOf
json.Unmarshal(rawTrack.RemixOf, &remixOf)
Expand All@@ -67,6 +70,18 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
collectSplitUserIds(rawTrack.DownloadConditions)
}

// Fetch accepted collaborators for these tracks in one query, and fold
// their user IDs into the bulk user fetch below so each is fully resolved.
collaboratorRows, err := q.GetTrackCollaborators(ctx, trackIds)
if err != nil {
return nil, err
}
collaboratorsByTrack := map[int32][]int32{}
for _, cr := range collaboratorRows {
collaboratorsByTrack[cr.TrackID] = append(collaboratorsByTrack[cr.TrackID], cr.CollaboratorUserID)
userIds = append(userIds, cr.CollaboratorUserID)
}

userMap, err := q.UsersKeyed(ctx, GetUsersParams{
MyID: arg.MyID.(int32),
Ids: userIds,
Expand DownExpand Up@@ -130,6 +145,14 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// Resolve accepted collaborators (order preserved from the query).
collaborators := []User{}
for _, cid := range collaboratorsByTrack[rawTrack.TrackID] {
if cu, ok := userMap[cid]; ok {
collaborators = append(collaborators, cu)
}
}

// Get access from the bulk access map
access := accessMap[rawTrack.TrackID]

Expand DownExpand Up@@ -179,6 +202,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
Preview: preview,
User: user,
UserID: user.ID,
Collaborators: collaborators,
FolloweeFavorites: fullFolloweeFavorites(rawTrack.FolloweeFavorites),
FolloweeReposts: fullFolloweeReposts(rawTrack.FolloweeReposts),
RemixOf: fullRemixOf,
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -432,6 +432,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/:userId/balance/history", app.v1UsersBalanceHistory)
g.Get("/users/:userId/managers", app.v1UsersManagers)
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
g.Get("/users/:userId/collaboration_invites", app.v1UsersCollaborationInvites)
g.Get("/grantees/:address/users", app.v1GranteeUsers)
g.Post("/users/:userId/grants", app.requireAuthMiddleware, app.requireWriteScope, app.postV1UsersGrant)
g.Delete("/users/:userId/grants/:address", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1UsersGrant)
Expand Down
4 changes: 3 additions & 1 deletion api/v1_notifications.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const notificationRelatedActorsPerGroup = 1
type GetNotificationsQueryParams struct {
// Note that when limit is 0, we return 20 items to calculate unread count
Limit int `query:"limit" default:"20" validate:"min=0,max=100"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request track_collaborator_invite track_collaborator_accept claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
GroupID string `query:"group_id" validate:"omitempty"`
Timestamp float64 `query:"timestamp" validate:"omitempty,min=0"`
}
Expand DownExpand Up@@ -378,6 +378,8 @@ func collectNotificationRelatedIds(action json.RawMessage, userIds, trackIds, pl
"data.receiver_user_id",
"data.dethroned_user_id",
"data.grantee_user_id",
"data.inviter_user_id",
"data.collaborator_user_id",
"data.tastemaker_user_id",
"data.tastemaker_item_owner_id",
"data.track_owner_id",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
54 changes: 54 additions & 0 deletions api/dbv1/collaborator_invites.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
package dbv1

import (
"context"
"time"

"api.audius.co/trashid"
)

// FullTrackCollaboratorInvite is a collaborator credit from the invited user's
// perspective: which track, who invited them, and the current status.
type FullTrackCollaboratorInvite struct {
TrackID string `json:"track_id"`
Status string `json:"status"`
InvitedBy User `json:"invited_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FullTrackCollaboratorInvites resolves a user's collaborator invites, embedding
// the inviter as a full user object (mirrors FullManagers).
func (q *Queries) FullTrackCollaboratorInvites(ctx context.Context, params GetTrackCollaboratorInvitesForUserParams) ([]FullTrackCollaboratorInvite, error) {
rows, err := q.GetTrackCollaboratorInvitesForUser(ctx, params)
if err != nil {
return nil, err
}

inviterIds := make([]int32, len(rows))
for i, row := range rows {
inviterIds[i] = row.InvitedBy
}

users, err := q.UsersKeyed(ctx, GetUsersParams{
Ids: inviterIds,
MyID: params.UserID,
})
if err != nil {
return nil, err
}

invites := make([]FullTrackCollaboratorInvite, len(rows))
for i, row := range rows {
trackID, _ := trashid.EncodeHashId(int(row.TrackID))
invites[i] = FullTrackCollaboratorInvite{
TrackID: trackID,
Status: row.Status,
InvitedBy: users[row.InvitedBy],
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}

return invites, nil
}
24 changes: 21 additions & 3 deletions api/dbv1/models.go

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

20 changes: 20 additions & 0 deletions api/dbv1/queries/track_collaborators.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
-- Accepted collaborators for a set of tracks, used to embed a `collaborators`
-- array on track responses. Returns one row per (track, collaborator); the Go
-- layer bulk-resolves the user objects. Backed by the track_collaborators
-- primary key (track_id leads), so the ANY(...) lookup is index-served.
-- name: GetTrackCollaborators :many
SELECT track_id, collaborator_user_id
FROM track_collaborators
WHERE track_id = ANY(@track_ids::int[])
AND status = 'accepted'
ORDER BY track_id, created_at;

-- A user's collaborator invites/credits, optionally filtered by status
-- (pending/accepted/rejected). Backed by the covering
-- (collaborator_user_id, status, track_id) index.
-- name: GetTrackCollaboratorInvitesForUser :many
SELECT track_id, collaborator_user_id, invited_by, status, created_at, updated_at
FROM track_collaborators
WHERE collaborator_user_id = @user_id::int
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY created_at DESC;
102 changes: 102 additions & 0 deletions api/dbv1/track_collaborators.sql.go

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

42 changes: 33 additions & 9 deletions api/dbv1/tracks.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,16 @@ const IncludeID3TagsCtxKey = "includeID3Tags"
type Track struct {
GetTracksRow

Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Access Access `json:"access"`
Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Collaborators []User `json:"collaborators"`
Access Access `json:"access"`

FolloweeReposts []*FolloweeRepost `json:"followee_reposts"`
FolloweeFavorites []*FolloweeFavorite `json:"followee_favorites"`
Expand All@@ -45,6 +46,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

userIds := []int32{}
trackIds := make([]int32, 0, len(rawTracks))
collectSplitUserIds := func(usage *AccessGate) {
if usage == nil || usage.UsdcPurchase == nil {
return
Expand All@@ -56,6 +58,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

for _, rawTrack := range rawTracks {
userIds = append(userIds, rawTrack.UserID)
trackIds = append(trackIds, rawTrack.TrackID)

var remixOf RemixOf
json.Unmarshal(rawTrack.RemixOf, &remixOf)
Expand All@@ -67,6 +70,18 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
collectSplitUserIds(rawTrack.DownloadConditions)
}

// Fetch accepted collaborators for these tracks in one query, and fold
// their user IDs into the bulk user fetch below so each is fully resolved.
collaboratorRows, err := q.GetTrackCollaborators(ctx, trackIds)
if err != nil {
return nil, err
}
collaboratorsByTrack := map[int32][]int32{}
for _, cr := range collaboratorRows {
collaboratorsByTrack[cr.TrackID] = append(collaboratorsByTrack[cr.TrackID], cr.CollaboratorUserID)
userIds = append(userIds, cr.CollaboratorUserID)
}

userMap, err := q.UsersKeyed(ctx, GetUsersParams{
MyID: arg.MyID.(int32),
Ids: userIds,
Expand DownExpand Up@@ -130,6 +145,14 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// Resolve accepted collaborators (order preserved from the query).
collaborators := []User{}
for _, cid := range collaboratorsByTrack[rawTrack.TrackID] {
if cu, ok := userMap[cid]; ok {
collaborators = append(collaborators, cu)
}
}

// Get access from the bulk access map
access := accessMap[rawTrack.TrackID]

Expand DownExpand Up@@ -179,6 +202,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
Preview: preview,
User: user,
UserID: user.ID,
Collaborators: collaborators,
FolloweeFavorites: fullFolloweeFavorites(rawTrack.FolloweeFavorites),
FolloweeReposts: fullFolloweeReposts(rawTrack.FolloweeReposts),
RemixOf: fullRemixOf,
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -432,6 +432,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/:userId/balance/history", app.v1UsersBalanceHistory)
g.Get("/users/:userId/managers", app.v1UsersManagers)
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
g.Get("/users/:userId/collaboration_invites", app.v1UsersCollaborationInvites)
g.Get("/grantees/:address/users", app.v1GranteeUsers)
g.Post("/users/:userId/grants", app.requireAuthMiddleware, app.requireWriteScope, app.postV1UsersGrant)
g.Delete("/users/:userId/grants/:address", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1UsersGrant)
Expand Down
4 changes: 3 additions & 1 deletion api/v1_notifications.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const notificationRelatedActorsPerGroup = 1
type GetNotificationsQueryParams struct {
// Note that when limit is 0, we return 20 items to calculate unread count
Limit int `query:"limit" default:"20" validate:"min=0,max=100"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request track_collaborator_invite track_collaborator_accept claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
GroupID string `query:"group_id" validate:"omitempty"`
Timestamp float64 `query:"timestamp" validate:"omitempty,min=0"`
}
Expand DownExpand Up@@ -378,6 +378,8 @@ func collectNotificationRelatedIds(action json.RawMessage, userIds, trackIds, pl
"data.receiver_user_id",
"data.dethroned_user_id",
"data.grantee_user_id",
"data.inviter_user_id",
"data.collaborator_user_id",
"data.tastemaker_user_id",
"data.tastemaker_item_owner_id",
"data.track_owner_id",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
54 changes: 54 additions & 0 deletions api/dbv1/collaborator_invites.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
package dbv1

import (
"context"
"time"

"api.audius.co/trashid"
)

// FullTrackCollaboratorInvite is a collaborator credit from the invited user's
// perspective: which track, who invited them, and the current status.
type FullTrackCollaboratorInvite struct {
TrackID string `json:"track_id"`
Status string `json:"status"`
InvitedBy User `json:"invited_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FullTrackCollaboratorInvites resolves a user's collaborator invites, embedding
// the inviter as a full user object (mirrors FullManagers).
func (q *Queries) FullTrackCollaboratorInvites(ctx context.Context, params GetTrackCollaboratorInvitesForUserParams) ([]FullTrackCollaboratorInvite, error) {
rows, err := q.GetTrackCollaboratorInvitesForUser(ctx, params)
if err != nil {
return nil, err
}

inviterIds := make([]int32, len(rows))
for i, row := range rows {
inviterIds[i] = row.InvitedBy
}

users, err := q.UsersKeyed(ctx, GetUsersParams{
Ids: inviterIds,
MyID: params.UserID,
})
if err != nil {
return nil, err
}

invites := make([]FullTrackCollaboratorInvite, len(rows))
for i, row := range rows {
trackID, _ := trashid.EncodeHashId(int(row.TrackID))
invites[i] = FullTrackCollaboratorInvite{
TrackID: trackID,
Status: row.Status,
InvitedBy: users[row.InvitedBy],
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}

return invites, nil
}
24 changes: 21 additions & 3 deletions api/dbv1/models.go

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

20 changes: 20 additions & 0 deletions api/dbv1/queries/track_collaborators.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
-- Accepted collaborators for a set of tracks, used to embed a `collaborators`
-- array on track responses. Returns one row per (track, collaborator); the Go
-- layer bulk-resolves the user objects. Backed by the track_collaborators
-- primary key (track_id leads), so the ANY(...) lookup is index-served.
-- name: GetTrackCollaborators :many
SELECT track_id, collaborator_user_id
FROM track_collaborators
WHERE track_id = ANY(@track_ids::int[])
AND status = 'accepted'
ORDER BY track_id, created_at;

-- A user's collaborator invites/credits, optionally filtered by status
-- (pending/accepted/rejected). Backed by the covering
-- (collaborator_user_id, status, track_id) index.
-- name: GetTrackCollaboratorInvitesForUser :many
SELECT track_id, collaborator_user_id, invited_by, status, created_at, updated_at
FROM track_collaborators
WHERE collaborator_user_id = @user_id::int
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY created_at DESC;
102 changes: 102 additions & 0 deletions api/dbv1/track_collaborators.sql.go

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

42 changes: 33 additions & 9 deletions api/dbv1/tracks.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,16 @@ const IncludeID3TagsCtxKey = "includeID3Tags"
type Track struct {
GetTracksRow

Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Access Access `json:"access"`
Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Collaborators []User `json:"collaborators"`
Access Access `json:"access"`

FolloweeReposts []*FolloweeRepost `json:"followee_reposts"`
FolloweeFavorites []*FolloweeFavorite `json:"followee_favorites"`
Expand All@@ -45,6 +46,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

userIds := []int32{}
trackIds := make([]int32, 0, len(rawTracks))
collectSplitUserIds := func(usage *AccessGate) {
if usage == nil || usage.UsdcPurchase == nil {
return
Expand All@@ -56,6 +58,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

for _, rawTrack := range rawTracks {
userIds = append(userIds, rawTrack.UserID)
trackIds = append(trackIds, rawTrack.TrackID)

var remixOf RemixOf
json.Unmarshal(rawTrack.RemixOf, &remixOf)
Expand All@@ -67,6 +70,18 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
collectSplitUserIds(rawTrack.DownloadConditions)
}

// Fetch accepted collaborators for these tracks in one query, and fold
// their user IDs into the bulk user fetch below so each is fully resolved.
collaboratorRows, err := q.GetTrackCollaborators(ctx, trackIds)
if err != nil {
return nil, err
}
collaboratorsByTrack := map[int32][]int32{}
for _, cr := range collaboratorRows {
collaboratorsByTrack[cr.TrackID] = append(collaboratorsByTrack[cr.TrackID], cr.CollaboratorUserID)
userIds = append(userIds, cr.CollaboratorUserID)
}

userMap, err := q.UsersKeyed(ctx, GetUsersParams{
MyID: arg.MyID.(int32),
Ids: userIds,
Expand DownExpand Up@@ -130,6 +145,14 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// Resolve accepted collaborators (order preserved from the query).
collaborators := []User{}
for _, cid := range collaboratorsByTrack[rawTrack.TrackID] {
if cu, ok := userMap[cid]; ok {
collaborators = append(collaborators, cu)
}
}

// Get access from the bulk access map
access := accessMap[rawTrack.TrackID]

Expand DownExpand Up@@ -179,6 +202,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
Preview: preview,
User: user,
UserID: user.ID,
Collaborators: collaborators,
FolloweeFavorites: fullFolloweeFavorites(rawTrack.FolloweeFavorites),
FolloweeReposts: fullFolloweeReposts(rawTrack.FolloweeReposts),
RemixOf: fullRemixOf,
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -432,6 +432,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/:userId/balance/history", app.v1UsersBalanceHistory)
g.Get("/users/:userId/managers", app.v1UsersManagers)
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
g.Get("/users/:userId/collaboration_invites", app.v1UsersCollaborationInvites)
g.Get("/grantees/:address/users", app.v1GranteeUsers)
g.Post("/users/:userId/grants", app.requireAuthMiddleware, app.requireWriteScope, app.postV1UsersGrant)
g.Delete("/users/:userId/grants/:address", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1UsersGrant)
Expand Down
4 changes: 3 additions & 1 deletion api/v1_notifications.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const notificationRelatedActorsPerGroup = 1
type GetNotificationsQueryParams struct {
// Note that when limit is 0, we return 20 items to calculate unread count
Limit int `query:"limit" default:"20" validate:"min=0,max=100"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request track_collaborator_invite track_collaborator_accept claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
GroupID string `query:"group_id" validate:"omitempty"`
Timestamp float64 `query:"timestamp" validate:"omitempty,min=0"`
}
Expand DownExpand Up@@ -378,6 +378,8 @@ func collectNotificationRelatedIds(action json.RawMessage, userIds, trackIds, pl
"data.receiver_user_id",
"data.dethroned_user_id",
"data.grantee_user_id",
"data.inviter_user_id",
"data.collaborator_user_id",
"data.tastemaker_user_id",
"data.tastemaker_item_owner_id",
"data.track_owner_id",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
54 changes: 54 additions & 0 deletions api/dbv1/collaborator_invites.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
package dbv1

import (
"context"
"time"

"api.audius.co/trashid"
)

// FullTrackCollaboratorInvite is a collaborator credit from the invited user's
// perspective: which track, who invited them, and the current status.
type FullTrackCollaboratorInvite struct {
TrackID string `json:"track_id"`
Status string `json:"status"`
InvitedBy User `json:"invited_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FullTrackCollaboratorInvites resolves a user's collaborator invites, embedding
// the inviter as a full user object (mirrors FullManagers).
func (q *Queries) FullTrackCollaboratorInvites(ctx context.Context, params GetTrackCollaboratorInvitesForUserParams) ([]FullTrackCollaboratorInvite, error) {
rows, err := q.GetTrackCollaboratorInvitesForUser(ctx, params)
if err != nil {
return nil, err
}

inviterIds := make([]int32, len(rows))
for i, row := range rows {
inviterIds[i] = row.InvitedBy
}

users, err := q.UsersKeyed(ctx, GetUsersParams{
Ids: inviterIds,
MyID: params.UserID,
})
if err != nil {
return nil, err
}

invites := make([]FullTrackCollaboratorInvite, len(rows))
for i, row := range rows {
trackID, _ := trashid.EncodeHashId(int(row.TrackID))
invites[i] = FullTrackCollaboratorInvite{
TrackID: trackID,
Status: row.Status,
InvitedBy: users[row.InvitedBy],
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}

return invites, nil
}
24 changes: 21 additions & 3 deletions api/dbv1/models.go

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

20 changes: 20 additions & 0 deletions api/dbv1/queries/track_collaborators.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
-- Accepted collaborators for a set of tracks, used to embed a `collaborators`
-- array on track responses. Returns one row per (track, collaborator); the Go
-- layer bulk-resolves the user objects. Backed by the track_collaborators
-- primary key (track_id leads), so the ANY(...) lookup is index-served.
-- name: GetTrackCollaborators :many
SELECT track_id, collaborator_user_id
FROM track_collaborators
WHERE track_id = ANY(@track_ids::int[])
AND status = 'accepted'
ORDER BY track_id, created_at;

-- A user's collaborator invites/credits, optionally filtered by status
-- (pending/accepted/rejected). Backed by the covering
-- (collaborator_user_id, status, track_id) index.
-- name: GetTrackCollaboratorInvitesForUser :many
SELECT track_id, collaborator_user_id, invited_by, status, created_at, updated_at
FROM track_collaborators
WHERE collaborator_user_id = @user_id::int
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY created_at DESC;
102 changes: 102 additions & 0 deletions api/dbv1/track_collaborators.sql.go

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

42 changes: 33 additions & 9 deletions api/dbv1/tracks.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,16 @@ const IncludeID3TagsCtxKey = "includeID3Tags"
type Track struct {
GetTracksRow

Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Access Access `json:"access"`
Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Collaborators []User `json:"collaborators"`
Access Access `json:"access"`

FolloweeReposts []*FolloweeRepost `json:"followee_reposts"`
FolloweeFavorites []*FolloweeFavorite `json:"followee_favorites"`
Expand All@@ -45,6 +46,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

userIds := []int32{}
trackIds := make([]int32, 0, len(rawTracks))
collectSplitUserIds := func(usage *AccessGate) {
if usage == nil || usage.UsdcPurchase == nil {
return
Expand All@@ -56,6 +58,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

for _, rawTrack := range rawTracks {
userIds = append(userIds, rawTrack.UserID)
trackIds = append(trackIds, rawTrack.TrackID)

var remixOf RemixOf
json.Unmarshal(rawTrack.RemixOf, &remixOf)
Expand All@@ -67,6 +70,18 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
collectSplitUserIds(rawTrack.DownloadConditions)
}

// Fetch accepted collaborators for these tracks in one query, and fold
// their user IDs into the bulk user fetch below so each is fully resolved.
collaboratorRows, err := q.GetTrackCollaborators(ctx, trackIds)
if err != nil {
return nil, err
}
collaboratorsByTrack := map[int32][]int32{}
for _, cr := range collaboratorRows {
collaboratorsByTrack[cr.TrackID] = append(collaboratorsByTrack[cr.TrackID], cr.CollaboratorUserID)
userIds = append(userIds, cr.CollaboratorUserID)
}

userMap, err := q.UsersKeyed(ctx, GetUsersParams{
MyID: arg.MyID.(int32),
Ids: userIds,
Expand DownExpand Up@@ -130,6 +145,14 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// Resolve accepted collaborators (order preserved from the query).
collaborators := []User{}
for _, cid := range collaboratorsByTrack[rawTrack.TrackID] {
if cu, ok := userMap[cid]; ok {
collaborators = append(collaborators, cu)
}
}

// Get access from the bulk access map
access := accessMap[rawTrack.TrackID]

Expand DownExpand Up@@ -179,6 +202,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
Preview: preview,
User: user,
UserID: user.ID,
Collaborators: collaborators,
FolloweeFavorites: fullFolloweeFavorites(rawTrack.FolloweeFavorites),
FolloweeReposts: fullFolloweeReposts(rawTrack.FolloweeReposts),
RemixOf: fullRemixOf,
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -432,6 +432,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/:userId/balance/history", app.v1UsersBalanceHistory)
g.Get("/users/:userId/managers", app.v1UsersManagers)
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
g.Get("/users/:userId/collaboration_invites", app.v1UsersCollaborationInvites)
g.Get("/grantees/:address/users", app.v1GranteeUsers)
g.Post("/users/:userId/grants", app.requireAuthMiddleware, app.requireWriteScope, app.postV1UsersGrant)
g.Delete("/users/:userId/grants/:address", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1UsersGrant)
Expand Down
4 changes: 3 additions & 1 deletion api/v1_notifications.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const notificationRelatedActorsPerGroup = 1
type GetNotificationsQueryParams struct {
// Note that when limit is 0, we return 20 items to calculate unread count
Limit int `query:"limit" default:"20" validate:"min=0,max=100"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request track_collaborator_invite track_collaborator_accept claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
GroupID string `query:"group_id" validate:"omitempty"`
Timestamp float64 `query:"timestamp" validate:"omitempty,min=0"`
}
Expand DownExpand Up@@ -378,6 +378,8 @@ func collectNotificationRelatedIds(action json.RawMessage, userIds, trackIds, pl
"data.receiver_user_id",
"data.dethroned_user_id",
"data.grantee_user_id",
"data.inviter_user_id",
"data.collaborator_user_id",
"data.tastemaker_user_id",
"data.tastemaker_item_owner_id",
"data.track_owner_id",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
54 changes: 54 additions & 0 deletions api/dbv1/collaborator_invites.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
package dbv1

import (
"context"
"time"

"api.audius.co/trashid"
)

// FullTrackCollaboratorInvite is a collaborator credit from the invited user's
// perspective: which track, who invited them, and the current status.
type FullTrackCollaboratorInvite struct {
TrackID string `json:"track_id"`
Status string `json:"status"`
InvitedBy User `json:"invited_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FullTrackCollaboratorInvites resolves a user's collaborator invites, embedding
// the inviter as a full user object (mirrors FullManagers).
func (q *Queries) FullTrackCollaboratorInvites(ctx context.Context, params GetTrackCollaboratorInvitesForUserParams) ([]FullTrackCollaboratorInvite, error) {
rows, err := q.GetTrackCollaboratorInvitesForUser(ctx, params)
if err != nil {
return nil, err
}

inviterIds := make([]int32, len(rows))
for i, row := range rows {
inviterIds[i] = row.InvitedBy
}

users, err := q.UsersKeyed(ctx, GetUsersParams{
Ids: inviterIds,
MyID: params.UserID,
})
if err != nil {
return nil, err
}

invites := make([]FullTrackCollaboratorInvite, len(rows))
for i, row := range rows {
trackID, _ := trashid.EncodeHashId(int(row.TrackID))
invites[i] = FullTrackCollaboratorInvite{
TrackID: trackID,
Status: row.Status,
InvitedBy: users[row.InvitedBy],
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}

return invites, nil
}
24 changes: 21 additions & 3 deletions api/dbv1/models.go

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

20 changes: 20 additions & 0 deletions api/dbv1/queries/track_collaborators.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
-- Accepted collaborators for a set of tracks, used to embed a `collaborators`
-- array on track responses. Returns one row per (track, collaborator); the Go
-- layer bulk-resolves the user objects. Backed by the track_collaborators
-- primary key (track_id leads), so the ANY(...) lookup is index-served.
-- name: GetTrackCollaborators :many
SELECT track_id, collaborator_user_id
FROM track_collaborators
WHERE track_id = ANY(@track_ids::int[])
AND status = 'accepted'
ORDER BY track_id, created_at;

-- A user's collaborator invites/credits, optionally filtered by status
-- (pending/accepted/rejected). Backed by the covering
-- (collaborator_user_id, status, track_id) index.
-- name: GetTrackCollaboratorInvitesForUser :many
SELECT track_id, collaborator_user_id, invited_by, status, created_at, updated_at
FROM track_collaborators
WHERE collaborator_user_id = @user_id::int
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY created_at DESC;
102 changes: 102 additions & 0 deletions api/dbv1/track_collaborators.sql.go

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

42 changes: 33 additions & 9 deletions api/dbv1/tracks.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,16 @@ const IncludeID3TagsCtxKey = "includeID3Tags"
type Track struct {
GetTracksRow

Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Access Access `json:"access"`
Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Collaborators []User `json:"collaborators"`
Access Access `json:"access"`

FolloweeReposts []*FolloweeRepost `json:"followee_reposts"`
FolloweeFavorites []*FolloweeFavorite `json:"followee_favorites"`
Expand All@@ -45,6 +46,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

userIds := []int32{}
trackIds := make([]int32, 0, len(rawTracks))
collectSplitUserIds := func(usage *AccessGate) {
if usage == nil || usage.UsdcPurchase == nil {
return
Expand All@@ -56,6 +58,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

for _, rawTrack := range rawTracks {
userIds = append(userIds, rawTrack.UserID)
trackIds = append(trackIds, rawTrack.TrackID)

var remixOf RemixOf
json.Unmarshal(rawTrack.RemixOf, &remixOf)
Expand All@@ -67,6 +70,18 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
collectSplitUserIds(rawTrack.DownloadConditions)
}

// Fetch accepted collaborators for these tracks in one query, and fold
// their user IDs into the bulk user fetch below so each is fully resolved.
collaboratorRows, err := q.GetTrackCollaborators(ctx, trackIds)
if err != nil {
return nil, err
}
collaboratorsByTrack := map[int32][]int32{}
for _, cr := range collaboratorRows {
collaboratorsByTrack[cr.TrackID] = append(collaboratorsByTrack[cr.TrackID], cr.CollaboratorUserID)
userIds = append(userIds, cr.CollaboratorUserID)
}

userMap, err := q.UsersKeyed(ctx, GetUsersParams{
MyID: arg.MyID.(int32),
Ids: userIds,
Expand DownExpand Up@@ -130,6 +145,14 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// Resolve accepted collaborators (order preserved from the query).
collaborators := []User{}
for _, cid := range collaboratorsByTrack[rawTrack.TrackID] {
if cu, ok := userMap[cid]; ok {
collaborators = append(collaborators, cu)
}
}

// Get access from the bulk access map
access := accessMap[rawTrack.TrackID]

Expand DownExpand Up@@ -179,6 +202,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
Preview: preview,
User: user,
UserID: user.ID,
Collaborators: collaborators,
FolloweeFavorites: fullFolloweeFavorites(rawTrack.FolloweeFavorites),
FolloweeReposts: fullFolloweeReposts(rawTrack.FolloweeReposts),
RemixOf: fullRemixOf,
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -432,6 +432,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/:userId/balance/history", app.v1UsersBalanceHistory)
g.Get("/users/:userId/managers", app.v1UsersManagers)
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
g.Get("/users/:userId/collaboration_invites", app.v1UsersCollaborationInvites)
g.Get("/grantees/:address/users", app.v1GranteeUsers)
g.Post("/users/:userId/grants", app.requireAuthMiddleware, app.requireWriteScope, app.postV1UsersGrant)
g.Delete("/users/:userId/grants/:address", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1UsersGrant)
Expand Down
4 changes: 3 additions & 1 deletion api/v1_notifications.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const notificationRelatedActorsPerGroup = 1
type GetNotificationsQueryParams struct {
// Note that when limit is 0, we return 20 items to calculate unread count
Limit int `query:"limit" default:"20" validate:"min=0,max=100"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request track_collaborator_invite track_collaborator_accept claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
GroupID string `query:"group_id" validate:"omitempty"`
Timestamp float64 `query:"timestamp" validate:"omitempty,min=0"`
}
Expand DownExpand Up@@ -378,6 +378,8 @@ func collectNotificationRelatedIds(action json.RawMessage, userIds, trackIds, pl
"data.receiver_user_id",
"data.dethroned_user_id",
"data.grantee_user_id",
"data.inviter_user_id",
"data.collaborator_user_id",
"data.tastemaker_user_id",
"data.tastemaker_item_owner_id",
"data.track_owner_id",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
54 changes: 54 additions & 0 deletions api/dbv1/collaborator_invites.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
package dbv1

import (
"context"
"time"

"api.audius.co/trashid"
)

// FullTrackCollaboratorInvite is a collaborator credit from the invited user's
// perspective: which track, who invited them, and the current status.
type FullTrackCollaboratorInvite struct {
TrackID string `json:"track_id"`
Status string `json:"status"`
InvitedBy User `json:"invited_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FullTrackCollaboratorInvites resolves a user's collaborator invites, embedding
// the inviter as a full user object (mirrors FullManagers).
func (q *Queries) FullTrackCollaboratorInvites(ctx context.Context, params GetTrackCollaboratorInvitesForUserParams) ([]FullTrackCollaboratorInvite, error) {
rows, err := q.GetTrackCollaboratorInvitesForUser(ctx, params)
if err != nil {
return nil, err
}

inviterIds := make([]int32, len(rows))
for i, row := range rows {
inviterIds[i] = row.InvitedBy
}

users, err := q.UsersKeyed(ctx, GetUsersParams{
Ids: inviterIds,
MyID: params.UserID,
})
if err != nil {
return nil, err
}

invites := make([]FullTrackCollaboratorInvite, len(rows))
for i, row := range rows {
trackID, _ := trashid.EncodeHashId(int(row.TrackID))
invites[i] = FullTrackCollaboratorInvite{
TrackID: trackID,
Status: row.Status,
InvitedBy: users[row.InvitedBy],
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}

return invites, nil
}
24 changes: 21 additions & 3 deletions api/dbv1/models.go

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

20 changes: 20 additions & 0 deletions api/dbv1/queries/track_collaborators.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
-- Accepted collaborators for a set of tracks, used to embed a `collaborators`
-- array on track responses. Returns one row per (track, collaborator); the Go
-- layer bulk-resolves the user objects. Backed by the track_collaborators
-- primary key (track_id leads), so the ANY(...) lookup is index-served.
-- name: GetTrackCollaborators :many
SELECT track_id, collaborator_user_id
FROM track_collaborators
WHERE track_id = ANY(@track_ids::int[])
AND status = 'accepted'
ORDER BY track_id, created_at;

-- A user's collaborator invites/credits, optionally filtered by status
-- (pending/accepted/rejected). Backed by the covering
-- (collaborator_user_id, status, track_id) index.
-- name: GetTrackCollaboratorInvitesForUser :many
SELECT track_id, collaborator_user_id, invited_by, status, created_at, updated_at
FROM track_collaborators
WHERE collaborator_user_id = @user_id::int
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY created_at DESC;
102 changes: 102 additions & 0 deletions api/dbv1/track_collaborators.sql.go

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

42 changes: 33 additions & 9 deletions api/dbv1/tracks.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,16 @@ const IncludeID3TagsCtxKey = "includeID3Tags"
type Track struct {
GetTracksRow

Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Access Access `json:"access"`
Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Collaborators []User `json:"collaborators"`
Access Access `json:"access"`

FolloweeReposts []*FolloweeRepost `json:"followee_reposts"`
FolloweeFavorites []*FolloweeFavorite `json:"followee_favorites"`
Expand All@@ -45,6 +46,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

userIds := []int32{}
trackIds := make([]int32, 0, len(rawTracks))
collectSplitUserIds := func(usage *AccessGate) {
if usage == nil || usage.UsdcPurchase == nil {
return
Expand All@@ -56,6 +58,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

for _, rawTrack := range rawTracks {
userIds = append(userIds, rawTrack.UserID)
trackIds = append(trackIds, rawTrack.TrackID)

var remixOf RemixOf
json.Unmarshal(rawTrack.RemixOf, &remixOf)
Expand All@@ -67,6 +70,18 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
collectSplitUserIds(rawTrack.DownloadConditions)
}

// Fetch accepted collaborators for these tracks in one query, and fold
// their user IDs into the bulk user fetch below so each is fully resolved.
collaboratorRows, err := q.GetTrackCollaborators(ctx, trackIds)
if err != nil {
return nil, err
}
collaboratorsByTrack := map[int32][]int32{}
for _, cr := range collaboratorRows {
collaboratorsByTrack[cr.TrackID] = append(collaboratorsByTrack[cr.TrackID], cr.CollaboratorUserID)
userIds = append(userIds, cr.CollaboratorUserID)
}

userMap, err := q.UsersKeyed(ctx, GetUsersParams{
MyID: arg.MyID.(int32),
Ids: userIds,
Expand DownExpand Up@@ -130,6 +145,14 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// Resolve accepted collaborators (order preserved from the query).
collaborators := []User{}
for _, cid := range collaboratorsByTrack[rawTrack.TrackID] {
if cu, ok := userMap[cid]; ok {
collaborators = append(collaborators, cu)
}
}

// Get access from the bulk access map
access := accessMap[rawTrack.TrackID]

Expand DownExpand Up@@ -179,6 +202,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
Preview: preview,
User: user,
UserID: user.ID,
Collaborators: collaborators,
FolloweeFavorites: fullFolloweeFavorites(rawTrack.FolloweeFavorites),
FolloweeReposts: fullFolloweeReposts(rawTrack.FolloweeReposts),
RemixOf: fullRemixOf,
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -432,6 +432,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/:userId/balance/history", app.v1UsersBalanceHistory)
g.Get("/users/:userId/managers", app.v1UsersManagers)
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
g.Get("/users/:userId/collaboration_invites", app.v1UsersCollaborationInvites)
g.Get("/grantees/:address/users", app.v1GranteeUsers)
g.Post("/users/:userId/grants", app.requireAuthMiddleware, app.requireWriteScope, app.postV1UsersGrant)
g.Delete("/users/:userId/grants/:address", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1UsersGrant)
Expand Down
4 changes: 3 additions & 1 deletion api/v1_notifications.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const notificationRelatedActorsPerGroup = 1
type GetNotificationsQueryParams struct {
// Note that when limit is 0, we return 20 items to calculate unread count
Limit int `query:"limit" default:"20" validate:"min=0,max=100"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request track_collaborator_invite track_collaborator_accept claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
GroupID string `query:"group_id" validate:"omitempty"`
Timestamp float64 `query:"timestamp" validate:"omitempty,min=0"`
}
Expand DownExpand Up@@ -378,6 +378,8 @@ func collectNotificationRelatedIds(action json.RawMessage, userIds, trackIds, pl
"data.receiver_user_id",
"data.dethroned_user_id",
"data.grantee_user_id",
"data.inviter_user_id",
"data.collaborator_user_id",
"data.tastemaker_user_id",
"data.tastemaker_item_owner_id",
"data.track_owner_id",
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
54 changes: 54 additions & 0 deletions api/dbv1/collaborator_invites.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
package dbv1

import (
"context"
"time"

"api.audius.co/trashid"
)

// FullTrackCollaboratorInvite is a collaborator credit from the invited user's
// perspective: which track, who invited them, and the current status.
type FullTrackCollaboratorInvite struct {
TrackID string `json:"track_id"`
Status string `json:"status"`
InvitedBy User `json:"invited_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FullTrackCollaboratorInvites resolves a user's collaborator invites, embedding
// the inviter as a full user object (mirrors FullManagers).
func (q *Queries) FullTrackCollaboratorInvites(ctx context.Context, params GetTrackCollaboratorInvitesForUserParams) ([]FullTrackCollaboratorInvite, error) {
rows, err := q.GetTrackCollaboratorInvitesForUser(ctx, params)
if err != nil {
return nil, err
}

inviterIds := make([]int32, len(rows))
for i, row := range rows {
inviterIds[i] = row.InvitedBy
}

users, err := q.UsersKeyed(ctx, GetUsersParams{
Ids: inviterIds,
MyID: params.UserID,
})
if err != nil {
return nil, err
}

invites := make([]FullTrackCollaboratorInvite, len(rows))
for i, row := range rows {
trackID, _ := trashid.EncodeHashId(int(row.TrackID))
invites[i] = FullTrackCollaboratorInvite{
TrackID: trackID,
Status: row.Status,
InvitedBy: users[row.InvitedBy],
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}

return invites, nil
}
24 changes: 21 additions & 3 deletions api/dbv1/models.go

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

20 changes: 20 additions & 0 deletions api/dbv1/queries/track_collaborators.sql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
-- Accepted collaborators for a set of tracks, used to embed a `collaborators`
-- array on track responses. Returns one row per (track, collaborator); the Go
-- layer bulk-resolves the user objects. Backed by the track_collaborators
-- primary key (track_id leads), so the ANY(...) lookup is index-served.
-- name: GetTrackCollaborators :many
SELECT track_id, collaborator_user_id
FROM track_collaborators
WHERE track_id = ANY(@track_ids::int[])
AND status = 'accepted'
ORDER BY track_id, created_at;

-- A user's collaborator invites/credits, optionally filtered by status
-- (pending/accepted/rejected). Backed by the covering
-- (collaborator_user_id, status, track_id) index.
-- name: GetTrackCollaboratorInvitesForUser :many
SELECT track_id, collaborator_user_id, invited_by, status, created_at, updated_at
FROM track_collaborators
WHERE collaborator_user_id = @user_id::int
AND (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY created_at DESC;
102 changes: 102 additions & 0 deletions api/dbv1/track_collaborators.sql.go

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

42 changes: 33 additions & 9 deletions api/dbv1/tracks.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,15 +21,16 @@ const IncludeID3TagsCtxKey = "includeID3Tags"
type Track struct {
GetTracksRow

Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Access Access `json:"access"`
Permalink string `json:"permalink"`
IsStreamable bool `json:"is_streamable"`
Artwork *SquareImage `json:"artwork"`
Stream *MediaLink `json:"stream"`
Download *MediaLink `json:"download"`
Preview *MediaLink `json:"preview"`
UserID trashid.HashId `json:"user_id"`
User User `json:"user"`
Collaborators []User `json:"collaborators"`
Access Access `json:"access"`

FolloweeReposts []*FolloweeRepost `json:"followee_reposts"`
FolloweeFavorites []*FolloweeFavorite `json:"followee_favorites"`
Expand All@@ -45,6 +46,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

userIds := []int32{}
trackIds := make([]int32, 0, len(rawTracks))
collectSplitUserIds := func(usage *AccessGate) {
if usage == nil || usage.UsdcPurchase == nil {
return
Expand All@@ -56,6 +58,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

for _, rawTrack := range rawTracks {
userIds = append(userIds, rawTrack.UserID)
trackIds = append(trackIds, rawTrack.TrackID)

var remixOf RemixOf
json.Unmarshal(rawTrack.RemixOf, &remixOf)
Expand All@@ -67,6 +70,18 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
collectSplitUserIds(rawTrack.DownloadConditions)
}

// Fetch accepted collaborators for these tracks in one query, and fold
// their user IDs into the bulk user fetch below so each is fully resolved.
collaboratorRows, err := q.GetTrackCollaborators(ctx, trackIds)
if err != nil {
return nil, err
}
collaboratorsByTrack := map[int32][]int32{}
for _, cr := range collaboratorRows {
collaboratorsByTrack[cr.TrackID] = append(collaboratorsByTrack[cr.TrackID], cr.CollaboratorUserID)
userIds = append(userIds, cr.CollaboratorUserID)
}

userMap, err := q.UsersKeyed(ctx, GetUsersParams{
MyID: arg.MyID.(int32),
Ids: userIds,
Expand DownExpand Up@@ -130,6 +145,14 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// Resolve accepted collaborators (order preserved from the query).
collaborators := []User{}
for _, cid := range collaboratorsByTrack[rawTrack.TrackID] {
if cu, ok := userMap[cid]; ok {
collaborators = append(collaborators, cu)
}
}

// Get access from the bulk access map
access := accessMap[rawTrack.TrackID]

Expand DownExpand Up@@ -179,6 +202,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
Preview: preview,
User: user,
UserID: user.ID,
Collaborators: collaborators,
FolloweeFavorites: fullFolloweeFavorites(rawTrack.FolloweeFavorites),
FolloweeReposts: fullFolloweeReposts(rawTrack.FolloweeReposts),
RemixOf: fullRemixOf,
Expand Down
1 change: 1 addition & 0 deletions api/server.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -432,6 +432,7 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/users/:userId/balance/history", app.v1UsersBalanceHistory)
g.Get("/users/:userId/managers", app.v1UsersManagers)
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
g.Get("/users/:userId/collaboration_invites", app.v1UsersCollaborationInvites)
g.Get("/grantees/:address/users", app.v1GranteeUsers)
g.Post("/users/:userId/grants", app.requireAuthMiddleware, app.requireWriteScope, app.postV1UsersGrant)
g.Delete("/users/:userId/grants/:address", app.requireAuthMiddleware, app.requireWriteScope, app.deleteV1UsersGrant)
Expand Down
4 changes: 3 additions & 1 deletion api/v1_notifications.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const notificationRelatedActorsPerGroup = 1
type GetNotificationsQueryParams struct {
// Note that when limit is 0, we return 20 items to calculate unread count
Limit int `query:"limit" default:"20" validate:"min=0,max=100"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
Types []string `query:"types" validate:"dive,oneof=announcement follow repost save remix cosign create tip_receive tip_send challenge_reward repost_of_repost save_of_repost tastemaker reaction supporter_dethroned supporter_rank_up supporting_rank_up milestone track_added_to_playlist tier_change trending trending_playlist trending_underground usdc_purchase_buyer usdc_purchase_seller track_added_to_purchased_album request_manager approve_manager_request track_collaborator_invite track_collaborator_accept claimable_reward comment comment_thread comment_mention comment_reaction listen_streak_reminder fan_remix_contest_started fan_remix_contest_ended fan_remix_contest_ending_soon fan_remix_contest_winners_selected fan_remix_contest_submission artist_remix_contest_ended artist_remix_contest_ending_soon artist_remix_contest_submissions fan_club_text_post remix_contest_update"`
GroupID string `query:"group_id" validate:"omitempty"`
Timestamp float64 `query:"timestamp" validate:"omitempty,min=0"`
}
Expand DownExpand Up@@ -378,6 +378,8 @@ func collectNotificationRelatedIds(action json.RawMessage, userIds, trackIds, pl
"data.receiver_user_id",
"data.dethroned_user_id",
"data.grantee_user_id",
"data.inviter_user_id",
"data.collaborator_user_id",
"data.tastemaker_user_id",
"data.tastemaker_item_owner_id",
"data.track_owner_id",
Expand Down
Loading
Loading