+87
-12
apps/api/src/xrpc/app/rocksky/feed/getNowPlayings.ts
+87
-12
apps/api/src/xrpc/app/rocksky/feed/getNowPlayings.ts
···
1
1
import type { Context } from "context";
2
-
import { Effect, pipe, Cache, Duration } from "effect";
2
+
import { desc, eq, sql } from "drizzle-orm";
3
+
import { Cache, Duration, Effect, pipe } from "effect";
3
4
import type { Server } from "lexicon";
4
5
import type { NowPlayingView } from "lexicon/types/app/rocksky/feed/defs";
5
6
import type { QueryParams } from "lexicon/types/app/rocksky/feed/getNowPlayings";
6
-
import { deepCamelCaseKeys } from "lib";
7
+
import albums from "schema/albums";
8
+
import artists from "schema/artists";
9
+
import scrobbles from "schema/scrobbles";
10
+
import tracks from "schema/tracks";
11
+
import users from "schema/users";
7
12
8
13
export default function (server: Server, ctx: Context) {
9
14
const nowPlayingCache = Cache.make({
···
13
18
pipe(
14
19
{ params, ctx },
15
20
retrieve,
21
+
Effect.map((data) => ({ data })),
16
22
Effect.flatMap(presentation),
17
23
Effect.retry({ times: 3 }),
18
-
Effect.timeout("120 seconds"),
24
+
Effect.timeout("120 seconds")
19
25
),
20
26
});
21
27
···
26
32
Effect.catchAll((err) => {
27
33
console.error(err);
28
34
return Effect.succeed({});
29
-
}),
35
+
})
30
36
);
31
37
server.app.rocksky.feed.getNowPlayings({
32
38
handler: async ({ params }) => {
···
39
45
});
40
46
}
41
47
42
-
const retrieve = ({ params, ctx }: { params: QueryParams; ctx: Context }) => {
48
+
const retrieve = ({
49
+
params,
50
+
ctx,
51
+
}: {
52
+
params: QueryParams;
53
+
ctx: Context;
54
+
}): Effect.Effect<NowPlayingRecord[], Error, never> => {
43
55
return Effect.tryPromise({
44
56
try: () =>
45
-
ctx.analytics.post("library.getDistinctScrobbles", {
46
-
pagination: {
47
-
skip: 0,
48
-
take: params.size,
49
-
},
50
-
}),
57
+
ctx.db
58
+
.select({
59
+
xataId: scrobbles.id,
60
+
trackId: tracks.id,
61
+
title: tracks.title,
62
+
artist: tracks.artist,
63
+
albumArtist: tracks.albumArtist,
64
+
album: tracks.album,
65
+
albumArt: tracks.albumArt,
66
+
handle: users.handle,
67
+
did: users.did,
68
+
avatar: users.avatar,
69
+
uri: scrobbles.uri,
70
+
trackUri: tracks.uri,
71
+
artistUri: artists.uri,
72
+
albumUri: albums.uri,
73
+
xataCreatedat: scrobbles.createdAt,
74
+
})
75
+
.from(scrobbles)
76
+
.leftJoin(artists, eq(scrobbles.artistId, artists.id))
77
+
.leftJoin(albums, eq(scrobbles.albumId, albums.id))
78
+
.leftJoin(tracks, eq(scrobbles.trackId, tracks.id))
79
+
.leftJoin(users, eq(scrobbles.userId, users.id))
80
+
.where(
81
+
sql`scrobbles.xata_createdat = (
82
+
SELECT MAX(inner_s.xata_createdat)
83
+
FROM scrobbles inner_s
84
+
WHERE inner_s.user_id = ${users.id}
85
+
)`
86
+
)
87
+
.orderBy(desc(scrobbles.createdAt))
88
+
.limit(params.size || 20)
89
+
.execute(),
51
90
catch: (error) =>
52
91
new Error(`Failed to retrieve now playing songs: ${error}`),
53
92
});
···
55
94
56
95
const presentation = ({
57
96
data,
97
+
}: {
98
+
data: NowPlayingRecord[];
58
99
}): Effect.Effect<{ nowPlayings: NowPlayingView[] }, never> => {
59
100
return Effect.sync(() => ({
60
-
nowPlayings: deepCamelCaseKeys(data),
101
+
nowPlayings: data.map((record) => ({
102
+
album: record.album,
103
+
albumArt: record.albumArt,
104
+
albumArtist: record.albumArtist,
105
+
albumUri: record.albumUri,
106
+
artist: record.artist,
107
+
artistUri: record.artistUri,
108
+
avatar: record.avatar,
109
+
createdAt: record.xataCreatedat.toISOString(),
110
+
did: record.did,
111
+
handle: record.handle,
112
+
id: record.trackId,
113
+
title: record.title,
114
+
trackId: record.trackId,
115
+
trackUri: record.trackUri,
116
+
uri: record.uri,
117
+
})),
61
118
}));
62
119
};
120
+
121
+
type NowPlayingRecord = {
122
+
xataId: string;
123
+
trackId: string;
124
+
title: string;
125
+
artist: string;
126
+
albumArtist: string;
127
+
album: string;
128
+
albumArt: string;
129
+
handle: string;
130
+
did: string;
131
+
avatar: string;
132
+
uri: string;
133
+
trackUri: string;
134
+
artistUri: string;
135
+
albumUri: string;
136
+
xataCreatedat: Date;
137
+
};