+1
-4
apps/amethyst/app/(tabs)/(stamp)/stamp/index.tsx
+1
-4
apps/amethyst/app/(tabs)/(stamp)/stamp/index.tsx
···
87
87
headerBackButtonDisplayMode: "generic",
88
88
}}
89
89
/>
90
-
{/* Search Form */}
90
+
i {/* Search Form */}
91
91
<View className="flex gap-2 max-w-2xl w-screen px-4">
92
-
93
92
<Text className="font-bold text-lg">Search for a track</Text>
94
93
<Input
95
94
placeholder="Track name..."
···
116
115
}}
117
116
/>
118
117
<Input
119
-
120
118
placeholder="Album name..."
121
119
value={searchFields.release}
122
120
onChangeText={(text) =>
···
129
127
}}
130
128
/>
131
129
<View className="flex-row gap-2 mt-2">
132
-
133
130
<Button
134
131
className="flex-1"
135
132
onPress={handleSearch}
+14
-11
apps/amethyst/app/(tabs)/(stamp)/stamp/submit.tsx
+14
-11
apps/amethyst/app/(tabs)/(stamp)/stamp/submit.tsx
···
19
19
import { ExternalLink } from "@/components/ExternalLink";
20
20
import { StampContext, StampContextValue, StampStep } from "./_layout";
21
21
import { Image } from "react-native";
22
+
import { Artist } from "@teal/lexicons/src/types/fm/teal/alpha/feed/defs";
22
23
23
24
type CardyBResponse = {
24
25
error: string;
···
126
127
};
127
128
128
129
const createPlayRecord = (result: MusicBrainzRecording): PlayRecord => {
129
-
let artistNames: string[] = [];
130
-
if (result["artist-credit"]) {
131
-
artistNames = result["artist-credit"].map((a) => a.artist.name);
132
-
} else {
133
-
throw new Error("Artist must be specified!");
134
-
}
130
+
let artists = result["artist-credit"]?.map(
131
+
(a) =>
132
+
({
133
+
artistName: a.artist.name,
134
+
artistMbId: a.artist.id,
135
+
}) as Artist,
136
+
);
137
+
138
+
console.log("artists", artists);
135
139
136
140
return {
137
141
trackName: result.title ?? "Unknown Title",
138
142
recordingMbId: result.id ?? undefined,
139
143
duration: result.length ? Math.floor(result.length / 1000) : undefined,
140
-
artistNames, // result["artist-credit"]?.[0]?.artist?.name ?? "Unknown Artist",
141
-
artistMbIds: result["artist-credit"]?.map((a) => a.artist.id) ?? undefined,
144
+
artists: artists,
142
145
releaseName: result.selectedRelease?.title ?? undefined,
143
146
releaseMbId: result.selectedRelease?.id ?? undefined,
144
147
isrc: result.isrcs?.[0] ?? undefined,
···
148
151
// TODO: update this based on version/git commit hash on build
149
152
submissionClientAgent: "tealtracker/0.0.1b",
150
153
playedTime: new Date().toISOString(),
151
-
};
154
+
} as PlayRecord;
152
155
};
153
156
154
157
export default function Submit() {
···
247
250
// lol this type
248
251
const rt = new RichText({
249
252
text: `💮 now playing:
250
-
${record.trackName} by ${record.artistNames.join(", ")}
253
+
${record.trackName} by ${record.artists?.map((a) => a.artistName).join(", ")}
251
254
252
255
powered by @teal.fm`,
253
256
});
···
257
260
let customUrl: string | undefined = embedInfo?.customUrl;
258
261
259
262
let releaseYear = selectedTrack.selectedRelease?.date?.split("-")[0];
260
-
let title = `${record.trackName} by ${record.artistNames.join(", ")}`;
263
+
let title = `${record.trackName} by ${record.artists?.map((a) => a.artistName).join(", ")}`;
261
264
let description = `Song${releaseYear ? " · " + releaseYear : ""}${
262
265
selectedTrack.length && " · " + ms2hms(selectedTrack.length)
263
266
}`;
+12
-14
apps/amethyst/components/play/actorPlaysView.tsx
+12
-14
apps/amethyst/components/play/actorPlaysView.tsx
···
1
-
2
-
import { useStore } from '@/stores/mainStore';
3
-
import { OutputSchema as ActorFeedResponse } from '@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed';
4
-
import { useEffect, useState } from 'react';
5
-
import { ScrollView } from 'react-native';
6
-
import { Text } from '@/components/ui/text';
7
-
import PlayView from './playView';
8
-
import { Agent } from '@atproto/api';
1
+
import { useStore } from "@/stores/mainStore";
2
+
import { OutputSchema as ActorFeedResponse } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed";
3
+
import { useEffect, useState } from "react";
4
+
import { ScrollView } from "react-native";
5
+
import { Text } from "@/components/ui/text";
6
+
import PlayView from "./playView";
7
+
import { Agent } from "@atproto/api";
9
8
10
9
interface ActorPlaysViewProps {
11
10
repo: string | undefined;
12
11
pdsAgent: Agent | null;
13
12
}
14
13
const ActorPlaysView = ({ repo, pdsAgent }: ActorPlaysViewProps) => {
15
-
const [play, setPlay] = useState<ActorFeedResponse['plays'] | null>(null);
14
+
const [play, setPlay] = useState<ActorFeedResponse["plays"] | null>(null);
16
15
const isReady = useStore((state) => state.isAgentReady);
17
16
const tealDid = useStore((state) => state.tealDid);
18
17
useEffect(() => {
19
18
if (pdsAgent) {
20
19
pdsAgent
21
20
.call(
22
-
'fm.teal.alpha.feed.getActorFeed',
21
+
"fm.teal.alpha.feed.getActorFeed",
23
22
{ authorDID: repo },
24
23
{},
25
-
{ headers: { 'atproto-proxy': tealDid + '#teal_fm_appview' } },
24
+
{ headers: { "atproto-proxy": tealDid + "#teal_fm_appview" } },
26
25
)
27
26
.then((res) => {
28
27
res.data.plays as ActorFeedResponse;
···
32
31
console.log(e);
33
32
});
34
33
} else {
35
-
console.log('No agent');
34
+
console.log("No agent");
36
35
}
37
36
}, [isReady, pdsAgent, repo, tealDid]);
38
37
if (!play) {
···
45
44
key={p.playedTime + p.trackName}
46
45
releaseTitle={p.releaseName}
47
46
trackTitle={p.trackName}
48
-
artistName={p.artistNames.join(', ')}
47
+
artistName={p.artists.map((a) => a.artistName).join(", ")}
49
48
releaseMbid={p.releaseMbId}
50
-
51
49
/>
52
50
))}
53
51
</ScrollView>
+1
-1
apps/amethyst/package.json
+1
-1
apps/amethyst/package.json
···
22
22
"@atproto/oauth-client": "^0.3.16",
23
23
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
24
24
"@expo/vector-icons": "^14.1.0",
25
-
"@gorhom/bottom-sheet": "^5.1.2",
25
+
"@gorhom/bottom-sheet": "^5.1.3",
26
26
"@react-native-async-storage/async-storage": "2.1.2",
27
27
"@react-native-picker/picker": "^2.11.0",
28
28
"@react-navigation/native": "^7.1.8",
+14
-13
apps/aqua/src/xrpc/feed/getActorFeed.ts
+14
-13
apps/aqua/src/xrpc/feed/getActorFeed.ts
···
1
-
import { TealContext } from '@/ctx';
2
-
import { artists, db, plays, playToArtists } from '@teal/db';
3
-
import { eq, and, lt, desc, sql } from 'drizzle-orm';
4
-
import { OutputSchema } from '@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed';
1
+
import { TealContext } from "@/ctx";
2
+
import { artists, db, plays, playToArtists } from "@teal/db";
3
+
import { eq, and, lt, desc, sql } from "drizzle-orm";
4
+
import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed";
5
5
6
6
export default async function getActorFeed(c: TealContext) {
7
7
const params = c.req.query();
8
8
if (!params.authorDID) {
9
-
throw new Error('authorDID is required');
9
+
throw new Error("authorDID is required");
10
10
}
11
11
12
12
let limit = 20;
13
13
14
14
if (params.limit) {
15
15
limit = Number(params.limit);
16
-
if (limit > 50) throw new Error('Limit is over max allowed.');
16
+
if (limit > 50) throw new Error("Limit is over max allowed.");
17
17
}
18
18
19
19
// 'and' is here for typing reasons
···
30
30
const cursorPlay = cursorResult[0]?.playedTime;
31
31
32
32
if (!cursorPlay) {
33
-
throw new Error('Cursor not found');
33
+
throw new Error("Cursor not found");
34
34
}
35
35
36
36
whereClause = and(whereClause, lt(plays.playedTime, cursorPlay as any));
···
62
62
AND pa.artist_name IS NOT NULL -- Ensure both are non-null
63
63
),
64
64
'[]'::jsonb -- Correct empty JSONB array literal
65
-
)`.as('artists'),
65
+
)`.as("artists"),
66
66
})
67
67
.from(plays)
68
68
.leftJoin(playToArtists, sql`${plays.uri} = ${playToArtists.playUri}`)
···
114
114
recordingMbId: recordingMbid ?? undefined,
115
115
duration: duration ?? undefined,
116
116
117
-
// For arrays derived from a guaranteed array, map is safe.
118
-
// The SQL query ensures `artists` is '[]'::jsonb if empty.
119
-
// The SQL query also ensures artist.name/mbid are NOT NULL within the jsonb_agg
120
-
artistNames: artists.map((artist) => artist.name), // Will be [] if artists is []
121
-
artistMbIds: artists.map((artist) => artist.mbid), // Will be [] if artists is []
117
+
artists: artists.map((a) => {
118
+
return {
119
+
artistMbId: a.mbid,
120
+
artistName: a.name,
121
+
};
122
+
}),
122
123
123
124
releaseName: releaseName ?? undefined,
124
125
releaseMbId: releaseMbid ?? undefined,
+6
-5
apps/aqua/src/xrpc/feed/getPlay.ts
+6
-5
apps/aqua/src/xrpc/feed/getPlay.ts
···
88
88
trackMbId,
89
89
recordingMbId,
90
90
duration,
91
-
// Replace these with actual artist data from the array
92
-
artistNames: artists.map((artist) => artist.name),
93
-
artistMbIds: artists.map((artist) => artist.mbid),
94
-
// Or, if you want to keep the full artist objects:
95
-
// artists: artists,
91
+
artists: artists.map((a) => {
92
+
return {
93
+
artistMbId: a.mbid,
94
+
artistName: a.name,
95
+
};
96
+
}),
96
97
releaseName,
97
98
releaseMbId,
98
99
isrc,
+4
packages/db/.drizzle/0006_glamorous_mephisto.sql
+4
packages/db/.drizzle/0006_glamorous_mephisto.sql
···
1
+
CREATE MATERIALIZED VIEW "public"."mv_top_artists_for_user_30days" AS (select "artists"."mbid", "artists"."name", count("plays"."uri") as "play_count" from "artists" inner join "play_to_artists" on "artists"."mbid" = "play_to_artists"."artist_mbid" inner join "plays" on "plays"."uri" = "play_to_artists"."play_uri" inner join "profiles" on "profiles"."did" = "plays"."did" where "plays"."played_time" >= NOW() - INTERVAL '30 days' group by "artists"."mbid", "artists"."name" order by count("plays"."uri") DESC);--> statement-breakpoint
2
+
CREATE MATERIALIZED VIEW "public"."mv_top_artists_for_user_7days" AS (select "artists"."mbid", "artists"."name", count("plays"."uri") as "play_count" from "artists" inner join "play_to_artists" on "artists"."mbid" = "play_to_artists"."artist_mbid" inner join "plays" on "plays"."uri" = "play_to_artists"."play_uri" inner join "profiles" on "profiles"."did" = "plays"."did" where "plays"."played_time" >= NOW() - INTERVAL '7 days' group by "artists"."mbid", "artists"."name" order by count("plays"."uri") DESC);--> statement-breakpoint
3
+
CREATE MATERIALIZED VIEW "public"."mv_top_releases_for_user_30days" AS (select "releases"."mbid", "releases"."name", count("plays"."uri") as "play_count" from "releases" inner join "plays" on "plays"."release_mbid" = "releases"."mbid" inner join "profiles" on "profiles"."did" = "plays"."did" where "plays"."played_time" >= NOW() - INTERVAL '30 days' group by "releases"."mbid", "releases"."name" order by count("plays"."uri") DESC);--> statement-breakpoint
4
+
CREATE MATERIALIZED VIEW "public"."mv_top_releases_for_user_7days" AS (select "releases"."mbid", "releases"."name", count("plays"."uri") as "play_count" from "releases" inner join "plays" on "plays"."release_mbid" = "releases"."mbid" inner join "profiles" on "profiles"."did" = "plays"."did" where "plays"."played_time" >= NOW() - INTERVAL '7 days' group by "releases"."mbid", "releases"."name" order by count("plays"."uri") DESC);
+602
packages/db/.drizzle/meta/0006_snapshot.json
+602
packages/db/.drizzle/meta/0006_snapshot.json
···
1
+
{
2
+
"id": "74bfc452-adc9-461f-8b15-75c8a0f19e20",
3
+
"prevId": "c4b1bdd0-5fea-44e4-b753-4f25193b9c87",
4
+
"version": "7",
5
+
"dialect": "postgresql",
6
+
"tables": {
7
+
"public.artists": {
8
+
"name": "artists",
9
+
"schema": "",
10
+
"columns": {
11
+
"mbid": {
12
+
"name": "mbid",
13
+
"type": "uuid",
14
+
"primaryKey": true,
15
+
"notNull": true
16
+
},
17
+
"name": {
18
+
"name": "name",
19
+
"type": "text",
20
+
"primaryKey": false,
21
+
"notNull": true
22
+
},
23
+
"play_count": {
24
+
"name": "play_count",
25
+
"type": "integer",
26
+
"primaryKey": false,
27
+
"notNull": false,
28
+
"default": 0
29
+
}
30
+
},
31
+
"indexes": {},
32
+
"foreignKeys": {},
33
+
"compositePrimaryKeys": {},
34
+
"uniqueConstraints": {},
35
+
"policies": {},
36
+
"checkConstraints": {},
37
+
"isRLSEnabled": false
38
+
},
39
+
"public.play_to_artists": {
40
+
"name": "play_to_artists",
41
+
"schema": "",
42
+
"columns": {
43
+
"artist_mbid": {
44
+
"name": "artist_mbid",
45
+
"type": "uuid",
46
+
"primaryKey": false,
47
+
"notNull": true
48
+
},
49
+
"artist_name": {
50
+
"name": "artist_name",
51
+
"type": "text",
52
+
"primaryKey": false,
53
+
"notNull": false
54
+
},
55
+
"play_uri": {
56
+
"name": "play_uri",
57
+
"type": "text",
58
+
"primaryKey": false,
59
+
"notNull": true
60
+
}
61
+
},
62
+
"indexes": {},
63
+
"foreignKeys": {
64
+
"play_to_artists_artist_mbid_artists_mbid_fk": {
65
+
"name": "play_to_artists_artist_mbid_artists_mbid_fk",
66
+
"tableFrom": "play_to_artists",
67
+
"tableTo": "artists",
68
+
"columnsFrom": [
69
+
"artist_mbid"
70
+
],
71
+
"columnsTo": [
72
+
"mbid"
73
+
],
74
+
"onDelete": "no action",
75
+
"onUpdate": "no action"
76
+
},
77
+
"play_to_artists_play_uri_plays_uri_fk": {
78
+
"name": "play_to_artists_play_uri_plays_uri_fk",
79
+
"tableFrom": "play_to_artists",
80
+
"tableTo": "plays",
81
+
"columnsFrom": [
82
+
"play_uri"
83
+
],
84
+
"columnsTo": [
85
+
"uri"
86
+
],
87
+
"onDelete": "no action",
88
+
"onUpdate": "no action"
89
+
}
90
+
},
91
+
"compositePrimaryKeys": {
92
+
"play_to_artists_play_uri_artist_mbid_pk": {
93
+
"name": "play_to_artists_play_uri_artist_mbid_pk",
94
+
"columns": [
95
+
"play_uri",
96
+
"artist_mbid"
97
+
]
98
+
}
99
+
},
100
+
"uniqueConstraints": {},
101
+
"policies": {},
102
+
"checkConstraints": {},
103
+
"isRLSEnabled": false
104
+
},
105
+
"public.plays": {
106
+
"name": "plays",
107
+
"schema": "",
108
+
"columns": {
109
+
"cid": {
110
+
"name": "cid",
111
+
"type": "text",
112
+
"primaryKey": false,
113
+
"notNull": true
114
+
},
115
+
"did": {
116
+
"name": "did",
117
+
"type": "text",
118
+
"primaryKey": false,
119
+
"notNull": true
120
+
},
121
+
"duration": {
122
+
"name": "duration",
123
+
"type": "integer",
124
+
"primaryKey": false,
125
+
"notNull": false
126
+
},
127
+
"isrc": {
128
+
"name": "isrc",
129
+
"type": "text",
130
+
"primaryKey": false,
131
+
"notNull": false
132
+
},
133
+
"music_service_base_domain": {
134
+
"name": "music_service_base_domain",
135
+
"type": "text",
136
+
"primaryKey": false,
137
+
"notNull": false
138
+
},
139
+
"origin_url": {
140
+
"name": "origin_url",
141
+
"type": "text",
142
+
"primaryKey": false,
143
+
"notNull": false
144
+
},
145
+
"played_time": {
146
+
"name": "played_time",
147
+
"type": "timestamp with time zone",
148
+
"primaryKey": false,
149
+
"notNull": false
150
+
},
151
+
"processed_time": {
152
+
"name": "processed_time",
153
+
"type": "timestamp with time zone",
154
+
"primaryKey": false,
155
+
"notNull": false,
156
+
"default": "now()"
157
+
},
158
+
"rkey": {
159
+
"name": "rkey",
160
+
"type": "text",
161
+
"primaryKey": false,
162
+
"notNull": true
163
+
},
164
+
"recording_mbid": {
165
+
"name": "recording_mbid",
166
+
"type": "uuid",
167
+
"primaryKey": false,
168
+
"notNull": false
169
+
},
170
+
"release_mbid": {
171
+
"name": "release_mbid",
172
+
"type": "uuid",
173
+
"primaryKey": false,
174
+
"notNull": false
175
+
},
176
+
"release_name": {
177
+
"name": "release_name",
178
+
"type": "text",
179
+
"primaryKey": false,
180
+
"notNull": false
181
+
},
182
+
"submission_client_agent": {
183
+
"name": "submission_client_agent",
184
+
"type": "text",
185
+
"primaryKey": false,
186
+
"notNull": false
187
+
},
188
+
"track_name": {
189
+
"name": "track_name",
190
+
"type": "text",
191
+
"primaryKey": false,
192
+
"notNull": true
193
+
},
194
+
"uri": {
195
+
"name": "uri",
196
+
"type": "text",
197
+
"primaryKey": true,
198
+
"notNull": true
199
+
}
200
+
},
201
+
"indexes": {},
202
+
"foreignKeys": {
203
+
"plays_recording_mbid_recordings_mbid_fk": {
204
+
"name": "plays_recording_mbid_recordings_mbid_fk",
205
+
"tableFrom": "plays",
206
+
"tableTo": "recordings",
207
+
"columnsFrom": [
208
+
"recording_mbid"
209
+
],
210
+
"columnsTo": [
211
+
"mbid"
212
+
],
213
+
"onDelete": "no action",
214
+
"onUpdate": "no action"
215
+
},
216
+
"plays_release_mbid_releases_mbid_fk": {
217
+
"name": "plays_release_mbid_releases_mbid_fk",
218
+
"tableFrom": "plays",
219
+
"tableTo": "releases",
220
+
"columnsFrom": [
221
+
"release_mbid"
222
+
],
223
+
"columnsTo": [
224
+
"mbid"
225
+
],
226
+
"onDelete": "no action",
227
+
"onUpdate": "no action"
228
+
}
229
+
},
230
+
"compositePrimaryKeys": {},
231
+
"uniqueConstraints": {},
232
+
"policies": {},
233
+
"checkConstraints": {},
234
+
"isRLSEnabled": false
235
+
},
236
+
"public.profiles": {
237
+
"name": "profiles",
238
+
"schema": "",
239
+
"columns": {
240
+
"did": {
241
+
"name": "did",
242
+
"type": "text",
243
+
"primaryKey": true,
244
+
"notNull": true
245
+
},
246
+
"handle": {
247
+
"name": "handle",
248
+
"type": "text",
249
+
"primaryKey": false,
250
+
"notNull": false
251
+
},
252
+
"display_name": {
253
+
"name": "display_name",
254
+
"type": "text",
255
+
"primaryKey": false,
256
+
"notNull": false
257
+
},
258
+
"description": {
259
+
"name": "description",
260
+
"type": "text",
261
+
"primaryKey": false,
262
+
"notNull": false
263
+
},
264
+
"description_facets": {
265
+
"name": "description_facets",
266
+
"type": "jsonb",
267
+
"primaryKey": false,
268
+
"notNull": false
269
+
},
270
+
"avatar": {
271
+
"name": "avatar",
272
+
"type": "text",
273
+
"primaryKey": false,
274
+
"notNull": false
275
+
},
276
+
"banner": {
277
+
"name": "banner",
278
+
"type": "text",
279
+
"primaryKey": false,
280
+
"notNull": false
281
+
},
282
+
"created_at": {
283
+
"name": "created_at",
284
+
"type": "timestamp with time zone",
285
+
"primaryKey": false,
286
+
"notNull": false
287
+
}
288
+
},
289
+
"indexes": {},
290
+
"foreignKeys": {},
291
+
"compositePrimaryKeys": {},
292
+
"uniqueConstraints": {},
293
+
"policies": {},
294
+
"checkConstraints": {},
295
+
"isRLSEnabled": false
296
+
},
297
+
"public.recordings": {
298
+
"name": "recordings",
299
+
"schema": "",
300
+
"columns": {
301
+
"mbid": {
302
+
"name": "mbid",
303
+
"type": "uuid",
304
+
"primaryKey": true,
305
+
"notNull": true
306
+
},
307
+
"name": {
308
+
"name": "name",
309
+
"type": "text",
310
+
"primaryKey": false,
311
+
"notNull": true
312
+
},
313
+
"play_count": {
314
+
"name": "play_count",
315
+
"type": "integer",
316
+
"primaryKey": false,
317
+
"notNull": false,
318
+
"default": 0
319
+
}
320
+
},
321
+
"indexes": {},
322
+
"foreignKeys": {},
323
+
"compositePrimaryKeys": {},
324
+
"uniqueConstraints": {},
325
+
"policies": {},
326
+
"checkConstraints": {},
327
+
"isRLSEnabled": false
328
+
},
329
+
"public.releases": {
330
+
"name": "releases",
331
+
"schema": "",
332
+
"columns": {
333
+
"mbid": {
334
+
"name": "mbid",
335
+
"type": "uuid",
336
+
"primaryKey": true,
337
+
"notNull": true
338
+
},
339
+
"name": {
340
+
"name": "name",
341
+
"type": "text",
342
+
"primaryKey": false,
343
+
"notNull": true
344
+
},
345
+
"play_count": {
346
+
"name": "play_count",
347
+
"type": "integer",
348
+
"primaryKey": false,
349
+
"notNull": false,
350
+
"default": 0
351
+
}
352
+
},
353
+
"indexes": {},
354
+
"foreignKeys": {},
355
+
"compositePrimaryKeys": {},
356
+
"uniqueConstraints": {},
357
+
"policies": {},
358
+
"checkConstraints": {},
359
+
"isRLSEnabled": false
360
+
},
361
+
"public.featured_items": {
362
+
"name": "featured_items",
363
+
"schema": "",
364
+
"columns": {
365
+
"did": {
366
+
"name": "did",
367
+
"type": "text",
368
+
"primaryKey": true,
369
+
"notNull": true
370
+
},
371
+
"mbid": {
372
+
"name": "mbid",
373
+
"type": "text",
374
+
"primaryKey": false,
375
+
"notNull": true
376
+
},
377
+
"type": {
378
+
"name": "type",
379
+
"type": "text",
380
+
"primaryKey": false,
381
+
"notNull": true
382
+
}
383
+
},
384
+
"indexes": {},
385
+
"foreignKeys": {},
386
+
"compositePrimaryKeys": {},
387
+
"uniqueConstraints": {},
388
+
"policies": {},
389
+
"checkConstraints": {},
390
+
"isRLSEnabled": false
391
+
}
392
+
},
393
+
"enums": {},
394
+
"schemas": {},
395
+
"sequences": {},
396
+
"roles": {},
397
+
"policies": {},
398
+
"views": {
399
+
"public.mv_artist_play_counts": {
400
+
"columns": {
401
+
"mbid": {
402
+
"name": "mbid",
403
+
"type": "uuid",
404
+
"primaryKey": true,
405
+
"notNull": true
406
+
},
407
+
"name": {
408
+
"name": "name",
409
+
"type": "text",
410
+
"primaryKey": false,
411
+
"notNull": true
412
+
}
413
+
},
414
+
"definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" left join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" left join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" group by \"artists\".\"mbid\", \"artists\".\"name\"",
415
+
"name": "mv_artist_play_counts",
416
+
"schema": "public",
417
+
"isExisting": false,
418
+
"materialized": true
419
+
},
420
+
"public.mv_global_play_count": {
421
+
"columns": {},
422
+
"definition": "select count(\"uri\") as \"total_plays\", count(distinct \"did\") as \"unique_listeners\" from \"plays\"",
423
+
"name": "mv_global_play_count",
424
+
"schema": "public",
425
+
"isExisting": false,
426
+
"materialized": true
427
+
},
428
+
"public.mv_recording_play_counts": {
429
+
"columns": {
430
+
"mbid": {
431
+
"name": "mbid",
432
+
"type": "uuid",
433
+
"primaryKey": true,
434
+
"notNull": true
435
+
},
436
+
"name": {
437
+
"name": "name",
438
+
"type": "text",
439
+
"primaryKey": false,
440
+
"notNull": true
441
+
}
442
+
},
443
+
"definition": "select \"recordings\".\"mbid\", \"recordings\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"recordings\" left join \"plays\" on \"plays\".\"recording_mbid\" = \"recordings\".\"mbid\" group by \"recordings\".\"mbid\", \"recordings\".\"name\"",
444
+
"name": "mv_recording_play_counts",
445
+
"schema": "public",
446
+
"isExisting": false,
447
+
"materialized": true
448
+
},
449
+
"public.mv_release_play_counts": {
450
+
"columns": {
451
+
"mbid": {
452
+
"name": "mbid",
453
+
"type": "uuid",
454
+
"primaryKey": true,
455
+
"notNull": true
456
+
},
457
+
"name": {
458
+
"name": "name",
459
+
"type": "text",
460
+
"primaryKey": false,
461
+
"notNull": true
462
+
}
463
+
},
464
+
"definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" left join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" group by \"releases\".\"mbid\", \"releases\".\"name\"",
465
+
"name": "mv_release_play_counts",
466
+
"schema": "public",
467
+
"isExisting": false,
468
+
"materialized": true
469
+
},
470
+
"public.mv_top_artists_30days": {
471
+
"columns": {
472
+
"mbid": {
473
+
"name": "mbid",
474
+
"type": "uuid",
475
+
"primaryKey": true,
476
+
"notNull": true
477
+
},
478
+
"name": {
479
+
"name": "name",
480
+
"type": "text",
481
+
"primaryKey": false,
482
+
"notNull": true
483
+
}
484
+
},
485
+
"definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" inner join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" inner join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"artists\".\"mbid\", \"artists\".\"name\" order by count(\"plays\".\"uri\") DESC",
486
+
"name": "mv_top_artists_30days",
487
+
"schema": "public",
488
+
"isExisting": false,
489
+
"materialized": true
490
+
},
491
+
"public.mv_top_artists_for_user_30days": {
492
+
"columns": {
493
+
"mbid": {
494
+
"name": "mbid",
495
+
"type": "uuid",
496
+
"primaryKey": true,
497
+
"notNull": true
498
+
},
499
+
"name": {
500
+
"name": "name",
501
+
"type": "text",
502
+
"primaryKey": false,
503
+
"notNull": true
504
+
}
505
+
},
506
+
"definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" inner join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" inner join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" inner join \"profiles\" on \"profiles\".\"did\" = \"plays\".\"did\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"artists\".\"mbid\", \"artists\".\"name\" order by count(\"plays\".\"uri\") DESC",
507
+
"name": "mv_top_artists_for_user_30days",
508
+
"schema": "public",
509
+
"isExisting": false,
510
+
"materialized": true
511
+
},
512
+
"public.mv_top_artists_for_user_7days": {
513
+
"columns": {
514
+
"mbid": {
515
+
"name": "mbid",
516
+
"type": "uuid",
517
+
"primaryKey": true,
518
+
"notNull": true
519
+
},
520
+
"name": {
521
+
"name": "name",
522
+
"type": "text",
523
+
"primaryKey": false,
524
+
"notNull": true
525
+
}
526
+
},
527
+
"definition": "select \"artists\".\"mbid\", \"artists\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"artists\" inner join \"play_to_artists\" on \"artists\".\"mbid\" = \"play_to_artists\".\"artist_mbid\" inner join \"plays\" on \"plays\".\"uri\" = \"play_to_artists\".\"play_uri\" inner join \"profiles\" on \"profiles\".\"did\" = \"plays\".\"did\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '7 days' group by \"artists\".\"mbid\", \"artists\".\"name\" order by count(\"plays\".\"uri\") DESC",
528
+
"name": "mv_top_artists_for_user_7days",
529
+
"schema": "public",
530
+
"isExisting": false,
531
+
"materialized": true
532
+
},
533
+
"public.mv_top_releases_30days": {
534
+
"columns": {
535
+
"mbid": {
536
+
"name": "mbid",
537
+
"type": "uuid",
538
+
"primaryKey": true,
539
+
"notNull": true
540
+
},
541
+
"name": {
542
+
"name": "name",
543
+
"type": "text",
544
+
"primaryKey": false,
545
+
"notNull": true
546
+
}
547
+
},
548
+
"definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" inner join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"releases\".\"mbid\", \"releases\".\"name\" order by count(\"plays\".\"uri\") DESC",
549
+
"name": "mv_top_releases_30days",
550
+
"schema": "public",
551
+
"isExisting": false,
552
+
"materialized": true
553
+
},
554
+
"public.mv_top_releases_for_user_30days": {
555
+
"columns": {
556
+
"mbid": {
557
+
"name": "mbid",
558
+
"type": "uuid",
559
+
"primaryKey": true,
560
+
"notNull": true
561
+
},
562
+
"name": {
563
+
"name": "name",
564
+
"type": "text",
565
+
"primaryKey": false,
566
+
"notNull": true
567
+
}
568
+
},
569
+
"definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" inner join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" inner join \"profiles\" on \"profiles\".\"did\" = \"plays\".\"did\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '30 days' group by \"releases\".\"mbid\", \"releases\".\"name\" order by count(\"plays\".\"uri\") DESC",
570
+
"name": "mv_top_releases_for_user_30days",
571
+
"schema": "public",
572
+
"isExisting": false,
573
+
"materialized": true
574
+
},
575
+
"public.mv_top_releases_for_user_7days": {
576
+
"columns": {
577
+
"mbid": {
578
+
"name": "mbid",
579
+
"type": "uuid",
580
+
"primaryKey": true,
581
+
"notNull": true
582
+
},
583
+
"name": {
584
+
"name": "name",
585
+
"type": "text",
586
+
"primaryKey": false,
587
+
"notNull": true
588
+
}
589
+
},
590
+
"definition": "select \"releases\".\"mbid\", \"releases\".\"name\", count(\"plays\".\"uri\") as \"play_count\" from \"releases\" inner join \"plays\" on \"plays\".\"release_mbid\" = \"releases\".\"mbid\" inner join \"profiles\" on \"profiles\".\"did\" = \"plays\".\"did\" where \"plays\".\"played_time\" >= NOW() - INTERVAL '7 days' group by \"releases\".\"mbid\", \"releases\".\"name\" order by count(\"plays\".\"uri\") DESC",
591
+
"name": "mv_top_releases_for_user_7days",
592
+
"schema": "public",
593
+
"isExisting": false,
594
+
"materialized": true
595
+
}
596
+
},
597
+
"_meta": {
598
+
"columns": {},
599
+
"schemas": {},
600
+
"tables": {}
601
+
}
602
+
}
+7
packages/db/.drizzle/meta/_journal.json
+7
packages/db/.drizzle/meta/_journal.json
+76
packages/db/schema.ts
+76
packages/db/schema.ts
···
174
174
mbid: text("mbid").notNull(),
175
175
type: text("type").notNull(),
176
176
});
177
+
178
+
export const mvTopArtistsForUser30Days = pgMaterializedView(
179
+
"mv_top_artists_for_user_30days",
180
+
).as((qb) => {
181
+
return qb
182
+
.select({
183
+
artistMbid: artists.mbid,
184
+
artistName: artists.name,
185
+
playCount: sql<number>`count(${plays.uri})`.as("play_count"),
186
+
})
187
+
.from(artists)
188
+
.innerJoin(
189
+
playToArtists,
190
+
sql`${artists.mbid} = ${playToArtists.artistMbid}`,
191
+
)
192
+
.innerJoin(plays, sql`${plays.uri} = ${playToArtists.playUri}`)
193
+
.innerJoin(profiles, sql`${profiles.did} = ${plays.did}`)
194
+
.where(sql`${plays.playedTime} >= NOW() - INTERVAL '30 days'`)
195
+
.groupBy(artists.mbid, artists.name)
196
+
.orderBy(sql`count(${plays.uri}) DESC`);
197
+
});
198
+
199
+
export const mvTopArtistsForUser7Days = pgMaterializedView(
200
+
"mv_top_artists_for_user_7days",
201
+
).as((qb) => {
202
+
return qb
203
+
.select({
204
+
artistMbid: artists.mbid,
205
+
artistName: artists.name,
206
+
playCount: sql<number>`count(${plays.uri})`.as("play_count"),
207
+
})
208
+
.from(artists)
209
+
.innerJoin(
210
+
playToArtists,
211
+
sql`${artists.mbid} = ${playToArtists.artistMbid}`,
212
+
)
213
+
.innerJoin(plays, sql`${plays.uri} = ${playToArtists.playUri}`)
214
+
.innerJoin(profiles, sql`${profiles.did} = ${plays.did}`)
215
+
.where(sql`${plays.playedTime} >= NOW() - INTERVAL '7 days'`)
216
+
.groupBy(artists.mbid, artists.name)
217
+
.orderBy(sql`count(${plays.uri}) DESC`);
218
+
});
219
+
220
+
export const mvTopReleasesForUser30Days = pgMaterializedView(
221
+
"mv_top_releases_for_user_30days",
222
+
).as((qb) => {
223
+
return qb
224
+
.select({
225
+
releaseMbid: releases.mbid,
226
+
releaseName: releases.name,
227
+
playCount: sql<number>`count(${plays.uri})`.as("play_count"),
228
+
})
229
+
.from(releases)
230
+
.innerJoin(plays, sql`${plays.releaseMbid} = ${releases.mbid}`)
231
+
.innerJoin(profiles, sql`${profiles.did} = ${plays.did}`)
232
+
.where(sql`${plays.playedTime} >= NOW() - INTERVAL '30 days'`)
233
+
.groupBy(releases.mbid, releases.name)
234
+
.orderBy(sql`count(${plays.uri}) DESC`);
235
+
});
236
+
237
+
export const mvTopReleasesForUser7Days = pgMaterializedView(
238
+
"mv_top_releases_for_user_7days",
239
+
).as((qb) => {
240
+
return qb
241
+
.select({
242
+
releaseMbid: releases.mbid,
243
+
releaseName: releases.name,
244
+
playCount: sql<number>`count(${plays.uri})`.as("play_count"),
245
+
})
246
+
.from(releases)
247
+
.innerJoin(plays, sql`${plays.releaseMbid} = ${releases.mbid}`)
248
+
.innerJoin(profiles, sql`${profiles.did} = ${plays.did}`)
249
+
.where(sql`${plays.playedTime} >= NOW() - INTERVAL '7 days'`)
250
+
.groupBy(releases.mbid, releases.name)
251
+
.orderBy(sql`count(${plays.uri}) DESC`);
252
+
});
+5
-5
pnpm-lock.yaml
+5
-5
pnpm-lock.yaml
···
46
46
specifier: ^14.1.0
47
47
version: 14.1.0(expo-font@13.3.1(expo@53.0.9(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0)))(babel-plugin-react-compiler@19.0.0-beta-37ed2a7-20241206)(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)
48
48
'@gorhom/bottom-sheet':
49
-
specifier: ^5.1.2
50
-
version: 5.1.2(@types/react@19.0.14)(react-native-gesture-handler@2.24.0(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-reanimated@3.17.5(@babel/core@7.26.0)(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)
49
+
specifier: ^5.1.3
50
+
version: 5.1.4(@types/react@19.0.14)(react-native-gesture-handler@2.24.0(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-reanimated@3.17.5(@babel/core@7.26.0)(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)
51
51
'@react-native-async-storage/async-storage':
52
52
specifier: 2.1.2
53
53
version: 2.1.2(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))
···
1820
1820
'@floating-ui/utils@0.2.8':
1821
1821
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
1822
1822
1823
-
'@gorhom/bottom-sheet@5.1.2':
1824
-
resolution: {integrity: sha512-5np8oL2krqAsVKLRE4YmtkZkyZeFiitoki72bEpVhZb8SRTNuAEeSbP3noq5srKpcRsboCr7uI+xmMyrWUd9kw==}
1823
+
'@gorhom/bottom-sheet@5.1.4':
1824
+
resolution: {integrity: sha512-A49fbCLL3wxDhGvEsMzHDpBF+BqVCbXHEhLJo9plPSAxNjjPJFzJ65axj95R38+iqML0gmXyawpZ45PD4EEMAw==}
1825
1825
peerDependencies:
1826
1826
'@types/react': '*'
1827
1827
'@types/react-native': '*'
···
8965
8965
8966
8966
'@floating-ui/utils@0.2.8': {}
8967
8967
8968
-
'@gorhom/bottom-sheet@5.1.2(@types/react@19.0.14)(react-native-gesture-handler@2.24.0(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-reanimated@3.17.5(@babel/core@7.26.0)(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)':
8968
+
'@gorhom/bottom-sheet@5.1.4(@types/react@19.0.14)(react-native-gesture-handler@2.24.0(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native-reanimated@3.17.5(@babel/core@7.26.0)(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)':
8969
8969
dependencies:
8970
8970
'@gorhom/portal': 1.0.14(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0)
8971
8971
invariant: 2.2.4