diff --git a/api/comms/chat.go b/api/comms/chat.go index 9888d20d..67202ce7 100644 --- a/api/comms/chat.go +++ b/api/comms/chat.go @@ -364,6 +364,8 @@ func getNewBlasts(tx dbv1.DBTX, ctx context.Context, arg getNewBlastsParams) ([] WHERE blast.audience = 'remixer_audience' AND og.owner_id = blast.from_user_id AND t.owner_id = @user_id + AND t.access_authorities IS NULL + AND og.access_authorities IS NULL AND ( blast.audience_content_id IS NULL OR ( diff --git a/api/comms/validator.go b/api/comms/validator.go index d533f54a..a554142b 100644 --- a/api/comms/validator.go +++ b/api/comms/validator.go @@ -611,6 +611,8 @@ func hasNewBlastFromUser(pool *dbv1.DBPools, ctx context.Context, userID int32, JOIN tracks og ON remixes.parent_track_id = og.track_id WHERE og.owner_id = blast.from_user_id AND t.owner_id = $1 + AND t.access_authorities IS NULL + AND og.access_authorities IS NULL AND ( blast.audience_content_id IS NULL OR ( diff --git a/api/comms_blasts.go b/api/comms_blasts.go index 5d9ef7b0..035b5ea4 100644 --- a/api/comms_blasts.go +++ b/api/comms_blasts.go @@ -64,6 +64,8 @@ func (app *ApiServer) getNewBlasts(c *fiber.Ctx) error { WHERE blast.audience = 'remixer_audience' AND og.owner_id = blast.from_user_id AND t.owner_id = @user_id + AND t.access_authorities IS NULL + AND og.access_authorities IS NULL AND ( blast.audience_content_id IS NULL OR ( diff --git a/api/dbv1/get_events.sql.go b/api/dbv1/get_events.sql.go index f93d710a..8f33c7ea 100644 --- a/api/dbv1/get_events.sql.go +++ b/api/dbv1/get_events.sql.go @@ -26,6 +26,7 @@ SELECT e.event_data AS 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 ($1::int[] = '{}' OR e.entity_id = ANY($1::int[])) AND ($2::int[] = '{}' OR e.event_id = ANY($2::int[])) diff --git a/api/dbv1/get_genres.sql.go b/api/dbv1/get_genres.sql.go index 3dbea145..eca10991 100644 --- a/api/dbv1/get_genres.sql.go +++ b/api/dbv1/get_genres.sql.go @@ -23,6 +23,7 @@ WHERE AND genre != '' AND is_current = TRUE AND created_at > $1 + AND access_authorities IS NULL GROUP BY genre ORDER BY diff --git a/api/dbv1/get_track_ids_by_isrc.sql.go b/api/dbv1/get_track_ids_by_isrc.sql.go index 94438bec..116a02e7 100644 --- a/api/dbv1/get_track_ids_by_isrc.sql.go +++ b/api/dbv1/get_track_ids_by_isrc.sql.go @@ -13,6 +13,7 @@ const getTrackIdsByISRC = `-- name: GetTrackIdsByISRC :many SELECT track_id FROM tracks WHERE isrc = ANY($1::text[]) + AND access_authorities IS NULL ` func (q *Queries) GetTrackIdsByISRC(ctx context.Context, isrcs []string) ([]int32, error) { diff --git a/api/dbv1/get_tracks.sql.go b/api/dbv1/get_tracks.sql.go index c5553a5c..9794f5ec 100644 --- a/api/dbv1/get_tracks.sql.go +++ b/api/dbv1/get_tracks.sql.go @@ -224,6 +224,7 @@ LEFT JOIN aggregate_plays on play_item_id = t.track_id LEFT JOIN track_routes on t.track_id = track_routes.track_id and track_routes.is_current = true WHERE (is_unlisted = false OR t.owner_id = $1 OR $2::bool = TRUE) AND t.track_id = ANY($3::int[]) + AND t.access_authorities IS NULL ORDER BY t.track_id ` diff --git a/api/dbv1/models.go b/api/dbv1/models.go index 7499836d..5e18b28b 100644 --- a/api/dbv1/models.go +++ b/api/dbv1/models.go @@ -2335,10 +2335,11 @@ type TrackRow struct { // Artist of the original song if this track is a cover CoverOriginalArtist pgtype.Text `json:"cover_original_artist"` // Indicates whether the track is owned by the user for publishing payouts - IsOwnedByUser bool `json:"is_owned_by_user"` - NoAiUse pgtype.Bool `json:"no_ai_use"` - ParentalWarning NullParentalWarningType `json:"parental_warning"` - TerritoryCodes []string `json:"territory_codes"` + IsOwnedByUser bool `json:"is_owned_by_user"` + NoAiUse pgtype.Bool `json:"no_ai_use"` + ParentalWarning NullParentalWarningType `json:"parental_warning"` + TerritoryCodes []string `json:"territory_codes"` + AccessAuthorities []string `json:"access_authorities"` } type TrackTrendingScore struct { diff --git a/api/dbv1/queries/get_events.sql b/api/dbv1/queries/get_events.sql index ac36d8f1..557ffa0f 100644 --- a/api/dbv1/queries/get_events.sql +++ b/api/dbv1/queries/get_events.sql @@ -12,6 +12,7 @@ SELECT e.event_data AS 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 (@entity_ids::int[] = '{}' OR e.entity_id = ANY(@entity_ids::int[])) AND (@event_ids::int[] = '{}' OR e.event_id = ANY(@event_ids::int[])) diff --git a/api/dbv1/queries/get_genres.sql b/api/dbv1/queries/get_genres.sql index c569ca4d..bd761aab 100644 --- a/api/dbv1/queries/get_genres.sql +++ b/api/dbv1/queries/get_genres.sql @@ -9,6 +9,7 @@ WHERE AND genre != '' AND is_current = TRUE AND created_at > @start_time + AND access_authorities IS NULL GROUP BY genre ORDER BY diff --git a/api/dbv1/queries/get_track_ids_by_isrc.sql b/api/dbv1/queries/get_track_ids_by_isrc.sql index 39696ca7..f377e27d 100644 --- a/api/dbv1/queries/get_track_ids_by_isrc.sql +++ b/api/dbv1/queries/get_track_ids_by_isrc.sql @@ -1,4 +1,5 @@ -- name: GetTrackIdsByISRC :many SELECT track_id FROM tracks -WHERE isrc = ANY(@isrcs::text[]); \ No newline at end of file +WHERE isrc = ANY(@isrcs::text[]) + AND access_authorities IS NULL; diff --git a/api/dbv1/queries/get_tracks.sql b/api/dbv1/queries/get_tracks.sql index ce2b6169..351b3190 100644 --- a/api/dbv1/queries/get_tracks.sql +++ b/api/dbv1/queries/get_tracks.sql @@ -209,5 +209,6 @@ LEFT JOIN aggregate_plays on play_item_id = t.track_id LEFT JOIN track_routes on t.track_id = track_routes.track_id and track_routes.is_current = true WHERE (is_unlisted = false OR t.owner_id = @my_id OR @include_unlisted::bool = TRUE) AND t.track_id = ANY(@ids::int[]) + AND t.access_authorities IS NULL ORDER BY t.track_id ; diff --git a/api/v1_events_test.go b/api/v1_events_test.go index 53665378..c0434358 100644 --- a/api/v1_events_test.go +++ b/api/v1_events_test.go @@ -86,3 +86,27 @@ func TestGetEventsExcludesDeletedTracks(t *testing.T) { "data.3.entity_id": trashid.MustEncodeHashID(101), }) } + +func TestGetEventsExcludesAccessAuthoritiesTracks(t *testing.T) { + app := testAppWithFixtures(t) + ctx := context.Background() + require.NotNil(t, app.writePool, "test requires write pool") + + // Set access_authorities on track 102 (which has event 6) so it is gated + _, err := app.writePool.Exec(ctx, `UPDATE tracks SET access_authorities = ARRAY['0x123']::text[] WHERE track_id = 102 AND is_current = true`) + require.NoError(t, err) + + var eventsResponse struct { + Data []dbv1.FullEvent + } + status, _ := testGet(t, app, "/v1/events", &eventsResponse) + assert.Equal(t, 200, status) + + // Event 6 is for entity_id 102; it must not appear when the track has access_authorities + entity102Hash := trashid.MustEncodeHashID(102) + for _, e := range eventsResponse.Data { + assert.NotEqual(t, entity102Hash, e.EntityId, "events for access_authorities track 102 must not be returned") + } + + assert.Len(t, eventsResponse.Data, 4, "expected 4 events after excluding event for access_authorities track") +} diff --git a/api/v1_metrics_genres_test.go b/api/v1_metrics_genres_test.go index 83b4faec..2eb21119 100644 --- a/api/v1_metrics_genres_test.go +++ b/api/v1_metrics_genres_test.go @@ -1,11 +1,13 @@ package api import ( + "context" "fmt" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMetricsGenres(t *testing.T) { @@ -41,3 +43,52 @@ func TestMetricsGenres(t *testing.T) { } } + +func TestMetricsGenresExcludesAccessAuthoritiesTracks(t *testing.T) { + app := testAppWithFixtures(t) + ctx := context.Background() + require.NotNil(t, app.writePool, "test requires write pool") + + // Get baseline Electronic count (use epoch so all fixture tracks are included) + url := fmt.Sprintf("/v1/metrics/genres?start_time=%d", 0) + var before struct { + Data []struct { + Name string `json:"name"` + Count int64 `json:"count"` + } + } + status, _ := testGet(t, app, url, &before) + require.Equal(t, 200, status) + + var electronicCountBefore int64 + for _, g := range before.Data { + if g.Name == "Electronic" { + electronicCountBefore = g.Count + break + } + } + require.Greater(t, electronicCountBefore, int64(0), "fixtures should have Electronic tracks") + + // Gate one Electronic track (track 100 is Electronic) + _, err := app.writePool.Exec(ctx, `UPDATE tracks SET access_authorities = ARRAY['0xgate']::text[] WHERE track_id = 100 AND is_current = true`) + require.NoError(t, err) + + var after struct { + Data []struct { + Name string `json:"name"` + Count int64 `json:"count"` + } + } + status, _ = testGet(t, app, url, &after) + require.Equal(t, 200, status) + + var electronicCountAfter int64 + for _, g := range after.Data { + if g.Name == "Electronic" { + electronicCountAfter = g.Count + break + } + } + assert.Equal(t, electronicCountBefore-1, electronicCountAfter, "genre count must exclude access_authorities tracks") +} + diff --git a/api/v1_playlists_trending.go b/api/v1_playlists_trending.go index ee9ebf2a..134b946e 100644 --- a/api/v1_playlists_trending.go +++ b/api/v1_playlists_trending.go @@ -62,6 +62,8 @@ func (app *ApiServer) v1PlaylistsTrending(c *fiber.Ctx) error { pt.is_removed = false AND t.is_delete = false AND t.is_current = true + AND t.stem_of IS NULL + AND t.access_authorities IS NULL ) SELECT playlist_id diff --git a/api/v1_resolve_test.go b/api/v1_resolve_test.go index c5571b43..508cfece 100644 --- a/api/v1_resolve_test.go +++ b/api/v1_resolve_test.go @@ -1,9 +1,11 @@ package api import ( + "context" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestResolveTrackURL(t *testing.T) { @@ -12,6 +14,14 @@ func TestResolveTrackURL(t *testing.T) { status, _ := testGet(t, app, "/v1/resolve?url=https://audius.co/TracksByPermalink/track-by-permalink") assert.Equal(t, 302, status) + // Resolve does not filter by access_authorities (no tracks join); filter happens in GetTracks. + // So a gated track still resolves (302), but fetching the track by ID returns empty. + require.NotNil(t, app.writePool, "test requires write pool") + _, err := app.writePool.Exec(context.Background(), `UPDATE tracks SET access_authorities = ARRAY['0xabc']::text[] WHERE track_id = 500 AND is_current = true`) + require.NoError(t, err) + status, _ = testGet(t, app, "/v1/resolve?url=https://audius.co/TracksByPermalink/track-by-permalink") + assert.Equal(t, 302, status, "resolve still redirects; GetTracks filters gated tracks when fetching by ID") + // Test failed track resolution status, _ = testGet(t, app, "/v1/resolve?url=https://audius.co/nonexistent/track") assert.Equal(t, 404, status) diff --git a/api/v1_track_comment_count.go b/api/v1_track_comment_count.go index 691e2126..ad1c0d96 100644 --- a/api/v1_track_comment_count.go +++ b/api/v1_track_comment_count.go @@ -16,7 +16,7 @@ func (app *ApiServer) v1TrackCommentCount(c *fiber.Ctx) error { track AS ( SELECT track_id, owner_id FROM tracks - WHERE track_id = @trackId + WHERE track_id = @trackId AND access_authorities IS NULL ), -- Users muted by high-karma users diff --git a/api/v1_track_comments.go b/api/v1_track_comments.go index 839c3bae..1c3e5061 100644 --- a/api/v1_track_comments.go +++ b/api/v1_track_comments.go @@ -14,7 +14,7 @@ func (app *ApiServer) v1TrackComments(c *fiber.Ctx) error { track AS ( SELECT track_id, owner_id FROM tracks - WHERE track_id = @track_id + WHERE track_id = @track_id AND access_authorities IS NULL ), -- Users muted by high-karma users diff --git a/api/v1_tracks_feeling_lucky.go b/api/v1_tracks_feeling_lucky.go index a7915fed..895e4d7b 100644 --- a/api/v1_tracks_feeling_lucky.go +++ b/api/v1_tracks_feeling_lucky.go @@ -31,6 +31,7 @@ func (app *ApiServer) v1TracksFeelingLucky(c *fiber.Ctx) error { "is_delete = false", "is_unlisted = false", "stem_of IS NULL", + "access_authorities IS NULL", "aggregate_plays.count >= 250", } if params.MinFollowers != 0 { diff --git a/api/v1_tracks_recent_premium.go b/api/v1_tracks_recent_premium.go index 83d5cf74..a69a3027 100644 --- a/api/v1_tracks_recent_premium.go +++ b/api/v1_tracks_recent_premium.go @@ -28,6 +28,8 @@ func (app *ApiServer) v1TracksRecentPremium(c *fiber.Ctx) error { is_unlisted = false AND is_available = true AND is_delete = false AND + stem_of IS NULL AND + access_authorities IS NULL AND (stream_conditions ? 'usdc_purchase' OR download_conditions ? 'usdc_purchase') AND created_at >= now() - interval '1 month' ORDER BY owner_id, created_at DESC diff --git a/api/v1_tracks_test.go b/api/v1_tracks_test.go index 8352bb2c..39edc68d 100644 --- a/api/v1_tracks_test.go +++ b/api/v1_tracks_test.go @@ -1,10 +1,12 @@ package api import ( + "context" "testing" "api.audius.co/api/dbv1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTracksEndpoint(t *testing.T) { @@ -33,3 +35,20 @@ func TestGetTracksByPermalink(t *testing.T) { "data.0.title": "track by permalink", }) } + +func TestGetTracksExcludesAccessAuthorities(t *testing.T) { + app := testAppWithFixtures(t) + ctx := context.Background() + require.NotNil(t, app.writePool, "test requires write pool") + + // Track 100 has title "T1" and is returned as id eYZmn. Set access_authorities so it is gated. + _, err := app.writePool.Exec(ctx, `UPDATE tracks SET access_authorities = ARRAY['0xgate']::text[] WHERE track_id = 100 AND is_current = true`) + require.NoError(t, err) + + var resp struct { + Data []dbv1.Track + } + status, _ := testGet(t, app, "/v1/full/tracks?id=eYZmn", &resp) + assert.Equal(t, 200, status) + assert.Len(t, resp.Data, 0, "tracks with access_authorities must not be returned") +} diff --git a/api/v1_users_feed.go b/api/v1_users_feed.go index 12befde6..b67bb5ff 100644 --- a/api/v1_users_feed.go +++ b/api/v1_users_feed.go @@ -92,6 +92,7 @@ func (app *ApiServer) v1UsersFeed(c *fiber.Ctx) error { AND is_unlisted = false AND is_delete = false AND stem_of is null + AND access_authorities IS NULL ) UNION ALL diff --git a/api/v1_users_listen_counts_monthly.go b/api/v1_users_listen_counts_monthly.go index ffe325e2..3e6820f7 100644 --- a/api/v1_users_listen_counts_monthly.go +++ b/api/v1_users_listen_counts_monthly.go @@ -25,7 +25,7 @@ func (app *ApiServer) v1UsersListenCountsMonthly(c *fiber.Ctx) error { SUM(count) AS count FROM aggregate_monthly_plays WHERE play_item_id IN ( - SELECT track_id from tracks where owner_id = @userId + SELECT track_id FROM tracks WHERE owner_id = @userId AND stem_of IS NULL AND access_authorities IS NULL ) AND timestamp >= @startTime AND timestamp < @endTime diff --git a/api/v1_users_recommended_tracks.go b/api/v1_users_recommended_tracks.go index b13556ff..1dd7c855 100644 --- a/api/v1_users_recommended_tracks.go +++ b/api/v1_users_recommended_tracks.go @@ -77,6 +77,8 @@ func (app *ApiServer) v1UsersRecommendedTracks(c *fiber.Ctx) error { t.is_unlisted = false AND t.is_current = true AND t.is_delete = false + AND t.stem_of IS NULL + AND t.access_authorities IS NULL AND u.is_deactivated = false ORDER BY random() LIMIT 10 diff --git a/api/v1_users_related.go b/api/v1_users_related.go index 3a530d43..0d3ac9be 100644 --- a/api/v1_users_related.go +++ b/api/v1_users_related.go @@ -49,6 +49,7 @@ func (app *ApiServer) v1UsersRelated(c *fiber.Ctx) error { AND t.is_unlisted IS false AND t.is_available IS true AND t.stem_of IS NULL + AND t.access_authorities IS NULL AND owner_id = @userId GROUP BY genre ORDER BY count(*) DESC @@ -101,6 +102,7 @@ func (app *ApiServer) v1UsersRelated(c *fiber.Ctx) error { AND is_unlisted = false AND is_available = true AND stem_of IS NULL + AND access_authorities IS NULL AND genre IS NOT NULL GROUP BY genre ORDER BY COUNT(*) DESC diff --git a/api/v1_users_tags.go b/api/v1_users_tags.go index d5041d50..75e3cf80 100644 --- a/api/v1_users_tags.go +++ b/api/v1_users_tags.go @@ -27,6 +27,7 @@ func (app *ApiServer) v1UsersTags(c *fiber.Ctx) error { AND is_unlisted = false AND is_delete = false AND stem_of is null + AND access_authorities IS NULL ) AS split_tags WHERE tag != '' GROUP BY tag diff --git a/api/v1_users_tracks.go b/api/v1_users_tracks.go index 944b9eff..f81e7b02 100644 --- a/api/v1_users_tracks.go +++ b/api/v1_users_tracks.go @@ -75,7 +75,8 @@ func (app *ApiServer) v1UserTracks(c *fiber.Ctx) error { AND t.is_delete = false AND t.is_available = true AND ` + trackFilter + ` - AND t.stem_of is null` + gateFilter + ` + AND t.stem_of is null + AND t.access_authorities IS NULL` + gateFilter + ` ORDER BY (CASE WHEN t.track_id = u.artist_pick_track_id THEN 0 ELSE 1 END), ` + orderClause + ` LIMIT @limit OFFSET @offset diff --git a/api/v1_users_tracks_ai_attributed.go b/api/v1_users_tracks_ai_attributed.go index ebfb32a2..18d7c5ed 100644 --- a/api/v1_users_tracks_ai_attributed.go +++ b/api/v1_users_tracks_ai_attributed.go @@ -71,6 +71,7 @@ func (app *ApiServer) v1UserTracksAiAttributed(c *fiber.Ctx) error { AND t.is_available = true AND ` + trackFilter + ` AND t.stem_of is null + AND t.access_authorities IS NULL ORDER BY ` + orderClause + ` LIMIT @limit OFFSET @offset diff --git a/api/v1_users_tracks_count.go b/api/v1_users_tracks_count.go index e11fab6c..4ae31a15 100644 --- a/api/v1_users_tracks_count.go +++ b/api/v1_users_tracks_count.go @@ -42,7 +42,8 @@ func (app *ApiServer) v1UserTracksCount(c *fiber.Ctx) error { AND t.is_delete = false AND t.is_available = true AND ` + trackFilter + ` - AND t.stem_of is null` + gateFilter + AND t.stem_of is null + AND t.access_authorities IS NULL` + gateFilter args := pgx.NamedArgs{ "user_id": userId, diff --git a/ddl/functions/chat_blast_audience.sql b/ddl/functions/chat_blast_audience.sql index 823ed27e..54f9d19f 100644 --- a/ddl/functions/chat_blast_audience.sql +++ b/ddl/functions/chat_blast_audience.sql @@ -26,7 +26,7 @@ BEGIN UNION - -- remixer_audience + -- remixer_audience (exclude tracks with access_authorities / programmable distribution) SELECT chat_blast.blast_id, t.owner_id AS to_user_id FROM tracks t JOIN remixes ON remixes.child_track_id = t.track_id @@ -35,6 +35,8 @@ BEGIN AND chat_blast.audience = 'remixer_audience' AND og.owner_id = chat_blast.from_user_id AND t.owner_id != chat_blast.from_user_id + AND t.access_authorities IS NULL + AND og.access_authorities IS NULL AND ( chat_blast.audience_content_id IS NULL OR ( diff --git a/ddl/functions/handle_track.sql b/ddl/functions/handle_track.sql index f8e08815..d39da7e3 100644 --- a/ddl/functions/handle_track.sql +++ b/ddl/functions/handle_track.sql @@ -3,7 +3,8 @@ begin return track.is_unlisted = false and track.is_available = true and track.is_delete = false - and track.stem_of is null; + and track.stem_of is null + and track.access_authorities is null; end $$ LANGUAGE plpgsql; @@ -37,6 +38,7 @@ begin and t.is_delete is false and t.is_available is true and t.stem_of is null + and t.access_authorities is null and t.owner_id = new.owner_id ) where user_id = new.owner_id diff --git a/sql/01_schema.sql b/sql/01_schema.sql index 8d259a98..0fd95f27 100644 --- a/sql/01_schema.sql +++ b/sql/01_schema.sql @@ -1293,7 +1293,7 @@ BEGIN UNION - -- remixer_audience + -- remixer_audience (exclude tracks with access_authorities / programmable distribution) SELECT chat_blast.blast_id, t.owner_id AS to_user_id FROM tracks t JOIN remixes ON remixes.child_track_id = t.track_id @@ -1302,6 +1302,8 @@ BEGIN AND chat_blast.audience = 'remixer_audience' AND og.owner_id = chat_blast.from_user_id AND t.owner_id != chat_blast.from_user_id + AND t.access_authorities IS NULL + AND og.access_authorities IS NULL AND ( chat_blast.audience_content_id IS NULL OR ( @@ -3808,6 +3810,7 @@ begin and t.is_delete is false and t.is_available is true and t.stem_of is null + and t.access_authorities is null and t.owner_id = new.owner_id ) where user_id = new.owner_id @@ -5011,6 +5014,7 @@ BEGIN and (t.is_delete is false) and (t.is_unlisted is false) and (t.stem_of is null) + and (t.access_authorities is null) ) with no data; create index trending_params_track_id_idx on public.trending_params using btree (track_id); @@ -5163,7 +5167,8 @@ begin return track.is_unlisted = false and track.is_available = true and track.is_delete = false - and track.stem_of is null; + and track.stem_of is null + and track.access_authorities is null; end $$; @@ -5281,6 +5286,7 @@ CREATE TABLE public.tracks ( no_ai_use boolean DEFAULT false, parental_warning public.parental_warning_type, territory_codes text[], + access_authorities text[], CONSTRAINT check_territory_codes CHECK (public.validate_territory_codes(territory_codes)) );