tangled
alpha
login
or
join now
rocksky.app
/
rocksky
97
fork
atom
A decentralized music tracking and discovery platform built on AT Protocol 🎵
rocksky.app
spotify
atproto
lastfm
musicbrainz
scrobbling
listenbrainz
97
fork
atom
overview
issues
7
pulls
pipelines
refactor getScrobbles
tsiry-sandratraina.com
2 months ago
17c42ca8
92f9acd5
+78
-84
1 changed file
expand all
collapse all
unified
split
apps
api
src
xrpc
app
rocksky
scrobble
getScrobbles.ts
+78
-84
apps/api/src/xrpc/app/rocksky/scrobble/getScrobbles.ts
···
1
1
import type { Context } from "context";
2
2
-
import { and, desc, eq, inArray } from "drizzle-orm";
2
2
+
import { desc, eq, inArray } from "drizzle-orm";
3
3
import { Effect, pipe } from "effect";
4
4
import type { Server } from "lexicon";
5
5
import type { ScrobbleViewBasic } from "lexicon/types/app/rocksky/scrobble/defs";
···
43
43
}): Effect.Effect<Scrobbles | undefined, Error> => {
44
44
return Effect.tryPromise({
45
45
try: async () => {
46
46
-
const baseQuery = ctx.db
47
47
-
.select()
48
48
-
.from(tables.scrobbles)
49
49
-
.leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id))
50
50
-
.leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id));
46
46
+
const filterDids = await getFilterDids(ctx, params);
51
47
52
52
-
if (params.did && params.following) {
53
53
-
const followedUsers = await ctx.db
54
54
-
.select({ subjectDid: tables.follows.subject_did })
55
55
-
.from(tables.follows)
56
56
-
.where(eq(tables.follows.follower_did, params.did))
57
57
-
.execute();
48
48
+
if (filterDids !== null && filterDids.length === 0) {
49
49
+
return [];
50
50
+
}
58
51
59
59
-
const followedDids = followedUsers.map((f) => f.subjectDid);
52
52
+
const scrobbles = await fetchScrobbles(ctx, params, filterDids);
53
53
+
return enrichWithLikes(ctx, scrobbles, params.did);
54
54
+
},
55
55
+
catch: (error) => new Error(`Failed to retrieve scrobbles: ${error}`),
56
56
+
});
57
57
+
};
60
58
61
61
-
if (followedDids.length > 0) {
62
62
-
const scrobbles = await baseQuery
63
63
-
.where(inArray(tables.users.did, followedDids))
64
64
-
.orderBy(desc(tables.scrobbles.timestamp))
65
65
-
.offset(params.offset || 0)
66
66
-
.limit(params.limit || 20)
67
67
-
.execute();
59
59
+
const getFilterDids = async (
60
60
+
ctx: Context,
61
61
+
params: QueryParams,
62
62
+
): Promise<string[] | null> => {
63
63
+
if (!params.did || !params.following) {
64
64
+
return null; // No filtering needed
65
65
+
}
68
66
69
69
-
const trackIds = scrobbles.map((row) => row.tracks?.id).filter(
70
70
-
Boolean,
71
71
-
);
72
72
-
73
73
-
const likes = await ctx.db
74
74
-
.select()
75
75
-
.from(tables.lovedTracks)
76
76
-
.leftJoin(
77
77
-
tables.users,
78
78
-
eq(tables.lovedTracks.userId, tables.users.id),
79
79
-
)
80
80
-
.where(inArray(tables.lovedTracks.trackId, trackIds))
81
81
-
.execute();
67
67
+
const followedUsers = await ctx.db
68
68
+
.select({ subjectDid: tables.follows.subject_did })
69
69
+
.from(tables.follows)
70
70
+
.where(eq(tables.follows.follower_did, params.did))
71
71
+
.execute();
82
72
83
83
-
const likesMap = new Map<string, { count: number; liked: boolean }>();
73
73
+
return followedUsers.map((f) => f.subjectDid);
74
74
+
};
84
75
85
85
-
for (const trackId of trackIds) {
86
86
-
const trackLikes = likes.filter(
87
87
-
(l) => l.loved_tracks.trackId === trackId,
88
88
-
);
89
89
-
likesMap.set(trackId, {
90
90
-
count: trackLikes.length,
91
91
-
liked: trackLikes.some((l) => l.users.did === params.did),
92
92
-
});
93
93
-
}
76
76
+
const fetchScrobbles = async (
77
77
+
ctx: Context,
78
78
+
params: QueryParams,
79
79
+
filterDids: string[] | null,
80
80
+
) => {
81
81
+
const baseQuery = ctx.db
82
82
+
.select()
83
83
+
.from(tables.scrobbles)
84
84
+
.leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id))
85
85
+
.leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id));
94
86
95
95
-
return scrobbles.map((row) => ({
96
96
-
...row,
97
97
-
likesCount: likesMap.get(row.tracks?.id)?.count ?? 0,
98
98
-
liked: likesMap.get(row.tracks?.id)?.liked ?? false,
99
99
-
}));
100
100
-
} else {
101
101
-
return [];
102
102
-
}
103
103
-
}
87
87
+
const query = filterDids
88
88
+
? baseQuery.where(inArray(tables.users.did, filterDids))
89
89
+
: baseQuery;
104
90
105
105
-
const scrobbles = await baseQuery
106
106
-
.orderBy(desc(tables.scrobbles.timestamp))
107
107
-
.offset(params.offset || 0)
108
108
-
.limit(params.limit || 20)
109
109
-
.execute();
91
91
+
return query
92
92
+
.orderBy(desc(tables.scrobbles.timestamp))
93
93
+
.offset(params.offset || 0)
94
94
+
.limit(params.limit || 20)
95
95
+
.execute();
96
96
+
};
110
97
111
111
-
const trackIds = scrobbles.map((row) => row.tracks?.id).filter(Boolean);
98
98
+
const enrichWithLikes = async (
99
99
+
ctx: Context,
100
100
+
scrobbles: Awaited<ReturnType<typeof fetchScrobbles>>,
101
101
+
currentUserDid?: string,
102
102
+
) => {
103
103
+
const trackIds = scrobbles
104
104
+
.map((row) => row.tracks?.id)
105
105
+
.filter((id): id is string => Boolean(id));
112
106
113
113
-
const likes = await ctx.db
114
114
-
.select()
115
115
-
.from(tables.lovedTracks)
116
116
-
.leftJoin(tables.users, eq(tables.lovedTracks.userId, tables.users.id))
117
117
-
.where(inArray(tables.lovedTracks.trackId, trackIds))
118
118
-
.execute();
107
107
+
if (trackIds.length === 0) {
108
108
+
return scrobbles.map((row) => ({ ...row, likesCount: 0, liked: false }));
109
109
+
}
119
110
120
120
-
const likesMap = new Map<string, { count: number; liked: boolean }>();
111
111
+
const likes = await ctx.db
112
112
+
.select()
113
113
+
.from(tables.lovedTracks)
114
114
+
.leftJoin(tables.users, eq(tables.lovedTracks.userId, tables.users.id))
115
115
+
.where(inArray(tables.lovedTracks.trackId, trackIds))
116
116
+
.execute();
121
117
122
122
-
for (const trackId of trackIds) {
123
123
-
const trackLikes = likes.filter(
124
124
-
(l) => l.loved_tracks.trackId === trackId,
125
125
-
);
126
126
-
likesMap.set(trackId, {
127
127
-
count: trackLikes.length,
128
128
-
liked: trackLikes.some((l) => l.users.did === params.did),
129
129
-
});
130
130
-
}
118
118
+
const likesMap = new Map<string, { count: number; liked: boolean }>();
131
119
132
132
-
return scrobbles.map((row) => ({
133
133
-
...row,
134
134
-
likesCount: likesMap.get(row.tracks?.id)?.count ?? 0,
135
135
-
liked: likesMap.get(row.tracks?.id)?.liked ?? false,
136
136
-
}));
137
137
-
},
120
120
+
for (const trackId of trackIds) {
121
121
+
const trackLikes = likes.filter(
122
122
+
(l) => l.loved_tracks.trackId === trackId,
123
123
+
);
124
124
+
likesMap.set(trackId, {
125
125
+
count: trackLikes.length,
126
126
+
liked: trackLikes.some((l) => l.users?.did === currentUserDid),
127
127
+
});
128
128
+
}
138
129
139
139
-
catch: (error) => new Error(`Failed to retrieve scrobbles: ${error}`),
140
140
-
});
130
130
+
return scrobbles.map((row) => ({
131
131
+
...row,
132
132
+
likesCount: likesMap.get(row.tracks?.id ?? "")?.count ?? 0,
133
133
+
liked: likesMap.get(row.tracks?.id ?? "")?.liked ?? false,
134
134
+
}));
141
135
};
142
136
143
137
const presentation = (
···
155
149
tags: [],
156
150
id: scrobbles.id,
157
151
trackUri: tracks.uri,
158
158
-
likesCount: likesCount,
159
159
-
liked: liked,
152
152
+
likesCount,
153
153
+
liked,
160
154
})),
161
155
}));
162
156
};