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
29 changes: 21 additions & 8 deletions api/dbv1/tracks.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,34 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}
}

// A track is streamable unless it was deleted or its owner is no longer
// active - either the artist deactivated their own account or the
// account was delisted by the trusted notifier.
isStreamable := !rawTrack.IsDelete && !user.IsDeactivated

// Two reasons to leave a media link nil, both with the same effect: the
// URL should never be handed out, and the endpoints report the track as
// unavailable instead.
//
// A track row can have empty cid columns (e.g. an upload-v2 row whose
// track_cid/orig_file_cid backfill never ran). Signing an empty cid
// produces a content-node URL that is guaranteed to 404, so leave the
// media link nil instead and let the endpoints report the track as
// unavailable.
// track_cid/orig_file_cid backfill never ran), and signing an empty cid
// produces a content-node URL that is guaranteed to 404.
//
// A non-streamable track is worse: the cid is real, so the signed URL
// works. The stream and download endpoints reject these, but that only
// closes those two routes - anyone reading the track response could
// still fetch the audio straight from the content node. Preview is
// included because a preview clip is still the artist's audio.
var stream *MediaLink
if access.Stream && rawTrack.TrackCid.String != "" {
if isStreamable && access.Stream && rawTrack.TrackCid.String != "" {
stream, err = mediaLink(rawTrack.TrackCid.String, rawTrack.TrackID, arg.MyID.(int32), id3Tags)
if err != nil {
return nil, err
}
}

var download *MediaLink
if rawTrack.IsDownloadable && access.Download {
if isStreamable && rawTrack.IsDownloadable && access.Download {
cid := rawTrack.OrigFileCid.String
if cid == "" {
cid = rawTrack.TrackCid.String
Expand All @@ -224,7 +237,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]
}

var preview *MediaLink
if rawTrack.PreviewCid.String != "" {
if isStreamable && rawTrack.PreviewCid.String != "" {
preview, err = mediaLink(rawTrack.PreviewCid.String, rawTrack.TrackID, arg.MyID.(int32), id3Tags)
if err != nil {
return nil, err
Expand All @@ -233,7 +246,7 @@ func (q *Queries) TracksKeyed(ctx context.Context, arg TracksParams) (map[int32]

track := Track{
GetTracksRow: rawTrack,
IsStreamable: !rawTrack.IsDelete && !user.IsDeactivated,
IsStreamable: isStreamable,
Permalink: fmt.Sprintf("/%s/%s", user.Handle.String, rawTrack.Slug.String),
Artwork: squareImageStruct(rawTrack.CoverArtSizes, rawTrack.CoverArt),
Stream: stream,
Expand Down
94 changes: 86 additions & 8 deletions api/v1_track_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"api.audius.co/api/dbv1"
"api.audius.co/database"
"api.audius.co/trashid"
"github.com/stretchr/testify/assert"
)
Expand All @@ -18,8 +19,8 @@ func TestGetTrack(t *testing.T) {
assert.Equal(t, 200, status)

jsonAssert(t, body, map[string]any{
"data.id": "eYJyn",
"data.title": "Culca Canyon",
"data.id": "eYJyn",
"data.title": "Culca Canyon",
"data.play_count": 0,
})
}
Expand All @@ -40,17 +41,17 @@ func TestGetTrackPersonalization(t *testing.T) {
// repost/save flags should be false.
_, body := testGet(t, app, "/v1/full/tracks/eYJyn?user_id=ML51L", &resp)
jsonAssert(t, body, map[string]any{
"data.id": "eYJyn",
"data.has_current_user_reposted": false,
"data.has_current_user_saved": false,
"data.id": "eYJyn",
"data.has_current_user_reposted": false,
"data.has_current_user_saved": false,
"data.followee_reposts.0.user_id": trashid.MustEncodeHashID(1),
})

_, body = testGet(t, app, "/v1/full/tracks/eYZmn?user_id=ML51L", &resp)
jsonAssert(t, body, map[string]any{
"data.id": "eYZmn",
"data.has_current_user_reposted": false,
"data.has_current_user_saved": false,
"data.id": "eYZmn",
"data.has_current_user_reposted": false,
"data.has_current_user_saved": false,
"data.followee_favorites.0.user_id": trashid.MustEncodeHashID(1),
})

Expand Down Expand Up @@ -173,3 +174,80 @@ func TestGetTrackUsdcPurchaseSelfAccess(t *testing.T) {
"data.access.download": true,
})
}

// A track whose owner is no longer active - the artist deactivated their own
// account, or the account was delisted by the trusted notifier - must not carry
// signed content-node URLs in its response. The stream and download endpoints
// already reject these, but the media links in the track response bypass those
// endpoints entirely: the cid is real, so the signed URL serves the full audio
// straight from the content node to anyone who reads the response.
func TestGetTrack_NonStreamableOmitsMediaLinks(t *testing.T) {
for _, tc := range []struct {
name string
track map[string]any
user map[string]any
}{
{
name: "deactivated owner",
track: map[string]any{
"track_id": 1, "owner_id": 1, "title": "Deactivated Owner",
"track_cid": "QmTrackCid", "orig_file_cid": "QmOrigCid",
"preview_cid": "QmPreviewCid", "is_downloadable": true,
},
user: map[string]any{"user_id": 1, "handle": "testuser1", "is_deactivated": true},
},
{
name: "deleted track",
track: map[string]any{
"track_id": 1, "owner_id": 1, "title": "Deleted",
"track_cid": "QmTrackCid", "orig_file_cid": "QmOrigCid",
"preview_cid": "QmPreviewCid", "is_downloadable": true,
"is_delete": true,
},
user: map[string]any{"user_id": 1, "handle": "testuser1"},
},
} {
t.Run(tc.name, func(t *testing.T) {
app := emptyTestApp(t)
database.Seed(app.pool.Replicas[0], database.FixtureMap{
"tracks": []map[string]any{tc.track},
"users": []map[string]any{tc.user},
})

var resp struct{ Data dbv1.Track }
status, _ := testGet(t, app, "/v1/full/tracks/"+trashid.MustEncodeHashID(1), &resp)
assert.Equal(t, 200, status)

assert.False(t, resp.Data.IsStreamable)
assert.Nil(t, resp.Data.Stream)
assert.Nil(t, resp.Data.Download)
assert.Nil(t, resp.Data.Preview)
})
}
}

// The guard above is scoped to non-streamable tracks: an ordinary track with an
// active owner must still get its signed media links.
func TestGetTrack_StreamableKeepsMediaLinks(t *testing.T) {
app := emptyTestApp(t)
database.Seed(app.pool.Replicas[0], database.FixtureMap{
"tracks": []map[string]any{
{
"track_id": 1, "owner_id": 1, "title": "Active Owner",
"track_cid": "QmTrackCid", "orig_file_cid": "QmOrigCid",
"preview_cid": "QmPreviewCid", "is_downloadable": true,
},
},
"users": []map[string]any{{"user_id": 1, "handle": "testuser1"}},
})

var resp struct{ Data dbv1.Track }
status, _ := testGet(t, app, "/v1/full/tracks/"+trashid.MustEncodeHashID(1), &resp)
assert.Equal(t, 200, status)

assert.True(t, resp.Data.IsStreamable)
assert.NotNil(t, resp.Data.Stream)
assert.NotNil(t, resp.Data.Download)
assert.NotNil(t, resp.Data.Preview)
assert.Contains(t, resp.Data.Stream.Url, "signature=")
}
Loading