Skip to content

Commit 4ebce50

Browse files
authored
Library: tracks, playlists, albums (#20)
* wip: track library * tracks library mostly works. * playlist library * album library route * cleanup
1 parent d8fac26 commit 4ebce50

9 files changed

Lines changed: 330 additions & 9 deletions

File tree

api/dbv1/full_tracks.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ func (q *Queries) FullTracksKeyed(ctx context.Context, arg GetTracksParams) (map
7070
preview = mediaLink(track.PreviewCid.String, track.TrackID, arg.MyID.(int32))
7171
}
7272

73+
// client dies if this field is nil
74+
// todo: what are default field visibility values?
75+
if track.FieldVisibility == nil {
76+
track.FieldVisibility = []byte(`{}`)
77+
}
78+
7379
fullTrack := FullTrack{
7480
GetTracksRow: track,
7581
IsStreamable: !track.IsDelete && !user.IsDeactivated,

api/dbv1/get_tracks.sql.go

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/dbv1/models.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/dbv1/queries/get_tracks.sql

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,7 @@ FROM tracks t
150150
JOIN aggregate_track using (track_id)
151151
LEFT JOIN aggregate_plays on play_item_id = t.track_id
152152
LEFT JOIN track_routes on t.track_id = track_routes.track_id and track_routes.is_current = true
153-
WHERE is_available = true
154-
AND (is_unlisted = false OR t.owner_id = @my_id)
153+
WHERE (is_unlisted = false OR t.owner_id = @my_id)
155154
AND t.track_id = ANY(@ids::int[])
156155
ORDER BY t.track_id
157156
;

api/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ func NewApiServer(config config.Config) *ApiServer {
134134
g.Get("/users/:userId", app.v1User)
135135
g.Get("/users/:userId/followers", app.v1UsersFollowers)
136136
g.Get("/users/:userId/following", app.v1UsersFollowing)
137+
g.Get("/users/:userId/library/tracks", app.v1UsersLibraryTracks)
138+
g.Get("/users/:userId/library/:playlistType", app.v1UsersLibraryPlaylists)
137139
g.Get("/users/:userId/mutuals", app.v1UsersMutuals)
138140
g.Get("/users/:userId/reposts", app.v1UsersReposts)
139141
g.Get("/users/:userId/related", app.v1UsersRelated)

api/server_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"bridgerton.audius.co/config"
1313
"github.com/jackc/pgx/v5"
1414
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
1516
)
1617

1718
var (
@@ -91,6 +92,17 @@ func Test200(t *testing.T) {
9192
"/v1/full/users?id=7eP5n&id=_some_invalid_hash_id",
9293
"/v1/full/users/7eP5n/followers",
9394
"/v1/full/users/7eP5n/following",
95+
96+
"/v1/full/users/7eP5n/library/tracks",
97+
"/v1/full/users/7eP5n/library/tracks?type=repost&sort_method=plays&sort_direction=asc",
98+
"/v1/full/users/7eP5n/library/tracks?type=favorite&sort_method=reposts&sort_direction=desc",
99+
"/v1/full/users/7eP5n/library/tracks?type=purchase",
100+
101+
"/v1/full/users/7eP5n/library/playlists",
102+
"/v1/full/users/7eP5n/library/playlists?type=repost&sort_method=plays&sort_direction=asc",
103+
"/v1/full/users/7eP5n/library/playlists?type=favorite&sort_method=reposts&sort_direction=desc",
104+
"/v1/full/users/7eP5n/library/albums?type=purchase&sort_method=saves",
105+
94106
"/v1/full/users/7eP5n/mutuals",
95107
"/v1/full/users/7eP5n/reposts",
96108
"/v1/full/users/7eP5n/related",
@@ -115,8 +127,8 @@ func Test200(t *testing.T) {
115127
}
116128

117129
for _, u := range urls {
118-
status, _ := testGet(t, u)
119-
assert.Equal(t, 200, status, u)
130+
status, body := testGet(t, u)
131+
require.Equal(t, 200, status, u+" "+string(body))
120132

121133
// also test as a user
122134
if strings.Contains(u, "?") {
@@ -126,7 +138,7 @@ func Test200(t *testing.T) {
126138
}
127139

128140
status, _ = testGet(t, u)
129-
assert.Equal(t, 200, status, u)
141+
require.Equal(t, 200, status, u+" "+string(body))
130142
}
131143
}
132144

api/v1_users_library_playlists.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package api
2+
3+
import (
4+
"time"
5+
6+
"bridgerton.audius.co/api/dbv1"
7+
"github.com/gofiber/fiber/v2"
8+
"github.com/jackc/pgx/v5"
9+
)
10+
11+
func (app *ApiServer) v1UsersLibraryPlaylists(c *fiber.Ctx) error {
12+
13+
playlistType := "playlist"
14+
if c.Params("playlistType") == "albums" {
15+
playlistType = "album"
16+
}
17+
18+
sortField := "item_created_at"
19+
switch c.Query("sort_method") {
20+
case "reposts":
21+
sortField = "aggregate_playlist.repost_count"
22+
case "saves":
23+
sortField = "aggregate_playlist.save_count"
24+
}
25+
26+
sortDirection := "DESC"
27+
if c.Query("sort_direction") == "asc" {
28+
sortDirection = "ASC"
29+
}
30+
31+
sql := `
32+
WITH playlist_actions AS (
33+
-- include "own" playlists
34+
SELECT
35+
playlist_id as item_id,
36+
created_at as item_created_at,
37+
false as is_purchase
38+
FROM playlists
39+
WHERE playlist_owner_id = @userId
40+
AND is_album = (@playlistType = 'album')
41+
AND is_delete = false
42+
AND @actionType in ('favorite', 'all')
43+
44+
UNION ALL
45+
46+
SELECT
47+
save_item_id as item_id,
48+
created_at as item_created_at,
49+
false as is_purchase
50+
FROM saves
51+
WHERE save_type != 'track'
52+
AND user_id = @userId
53+
AND is_delete = false
54+
AND @actionType in ('favorite', 'all')
55+
56+
UNION ALL
57+
58+
SELECT
59+
repost_item_id as item_id,
60+
created_at as item_created_at,
61+
false as is_purchase
62+
FROM reposts
63+
WHERE repost_type != 'track'
64+
AND user_id = @userId
65+
AND is_delete = false
66+
AND @actionType in ('repost', 'all')
67+
68+
UNION ALL
69+
70+
SELECT
71+
content_id as item_id,
72+
created_at as item_created_at,
73+
true as is_purchase
74+
FROM usdc_purchases
75+
WHERE content_type = @playlistType::usdc_purchase_content_type
76+
AND buyer_user_id = @userId
77+
AND @actionType in ('purchase', 'all')
78+
79+
),
80+
deduped as (
81+
SELECT
82+
item_id,
83+
max(item_created_at) as item_created_at,
84+
bool_or(is_purchase) as is_purchase
85+
FROM playlist_actions
86+
GROUP BY item_id
87+
)
88+
SELECT deduped.*
89+
FROM deduped
90+
JOIN playlists ON playlist_id = item_id
91+
LEFT JOIN aggregate_playlist USING (playlist_id)
92+
WHERE playlists.is_album = (@playlistType = 'album')
93+
ORDER BY ` + sortField + ` ` + sortDirection + `, item_id desc
94+
LIMIT @limit
95+
OFFSET @offset
96+
`
97+
98+
rows, err := app.pool.Query(c.Context(), sql, pgx.NamedArgs{
99+
"playlistType": playlistType,
100+
"userId": c.Locals("userId"),
101+
"actionType": c.Query("type", "all"),
102+
"limit": c.Query("limit", "50"),
103+
"offset": c.Query("offset", "0"),
104+
})
105+
if err != nil {
106+
return err
107+
}
108+
109+
type Activity struct {
110+
// Class string `json:"class"`
111+
ItemID int32 `json:"item_id"`
112+
ItemCreatedAt time.Time `json:"timestamp"`
113+
IsPurchase bool `json:"-"`
114+
115+
Item any `db:"-" json:"item"`
116+
}
117+
118+
items, err := pgx.CollectRows(rows, pgx.RowToStructByName[Activity])
119+
if err != nil {
120+
return err
121+
}
122+
123+
// get ids
124+
ids := []int32{}
125+
for _, i := range items {
126+
ids = append(ids, i.ItemID)
127+
}
128+
129+
// get playlists
130+
playlists, err := app.queries.FullPlaylistsKeyed(c.Context(), dbv1.GetPlaylistsParams{
131+
Ids: ids,
132+
MyID: app.getMyId(c),
133+
})
134+
135+
// attach
136+
for idx, item := range items {
137+
if p, ok := playlists[item.ItemID]; ok {
138+
// todo: python code does: exclude playlists with only hidden tracks and empty playlists
139+
140+
// python API doesn't attach tracks???
141+
p.Tracks = nil
142+
143+
item.Item = p
144+
items[idx] = item
145+
}
146+
}
147+
148+
return c.JSON(fiber.Map{
149+
"data": items,
150+
})
151+
}

0 commit comments

Comments
 (0)