Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

change everything to new format

Natalie B e4d116d6 2350756f

Changed files
+742 -53
apps
amethyst
app
(tabs)
(stamp)
components
aqua
src
packages
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 43 43 "when": 1742260281562, 44 44 "tag": "0005_plain_vulture", 45 45 "breakpoints": true 46 + }, 47 + { 48 + "idx": 6, 49 + "version": "7", 50 + "when": 1747602359287, 51 + "tag": "0006_glamorous_mephisto", 52 + "breakpoints": true 46 53 } 47 54 ] 48 55 }
+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
··· 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