A decentralized music tracking and discovery platform built on AT Protocol 🎵
listenbrainz spotify atproto lastfm musicbrainz scrobbling

Refactor getNowPlaying to use database queries and enhance data retrieval

Changed files
+87 -12
apps
api
src
xrpc
app
rocksky
+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 + };