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

refactor getScrobbles

+78 -84
+78 -84
apps/api/src/xrpc/app/rocksky/scrobble/getScrobbles.ts
··· 1 1 import type { Context } from "context"; 2 - import { and, desc, eq, inArray } from "drizzle-orm"; 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 - const baseQuery = ctx.db 47 - .select() 48 - .from(tables.scrobbles) 49 - .leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id)) 50 - .leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id)); 46 + const filterDids = await getFilterDids(ctx, params); 51 47 52 - if (params.did && params.following) { 53 - const followedUsers = await ctx.db 54 - .select({ subjectDid: tables.follows.subject_did }) 55 - .from(tables.follows) 56 - .where(eq(tables.follows.follower_did, params.did)) 57 - .execute(); 48 + if (filterDids !== null && filterDids.length === 0) { 49 + return []; 50 + } 58 51 59 - const followedDids = followedUsers.map((f) => f.subjectDid); 52 + const scrobbles = await fetchScrobbles(ctx, params, filterDids); 53 + return enrichWithLikes(ctx, scrobbles, params.did); 54 + }, 55 + catch: (error) => new Error(`Failed to retrieve scrobbles: ${error}`), 56 + }); 57 + }; 60 58 61 - if (followedDids.length > 0) { 62 - const scrobbles = await baseQuery 63 - .where(inArray(tables.users.did, followedDids)) 64 - .orderBy(desc(tables.scrobbles.timestamp)) 65 - .offset(params.offset || 0) 66 - .limit(params.limit || 20) 67 - .execute(); 59 + const getFilterDids = async ( 60 + ctx: Context, 61 + params: QueryParams, 62 + ): Promise<string[] | null> => { 63 + if (!params.did || !params.following) { 64 + return null; // No filtering needed 65 + } 68 66 69 - const trackIds = scrobbles.map((row) => row.tracks?.id).filter( 70 - Boolean, 71 - ); 72 - 73 - const likes = await ctx.db 74 - .select() 75 - .from(tables.lovedTracks) 76 - .leftJoin( 77 - tables.users, 78 - eq(tables.lovedTracks.userId, tables.users.id), 79 - ) 80 - .where(inArray(tables.lovedTracks.trackId, trackIds)) 81 - .execute(); 67 + const followedUsers = await ctx.db 68 + .select({ subjectDid: tables.follows.subject_did }) 69 + .from(tables.follows) 70 + .where(eq(tables.follows.follower_did, params.did)) 71 + .execute(); 82 72 83 - const likesMap = new Map<string, { count: number; liked: boolean }>(); 73 + return followedUsers.map((f) => f.subjectDid); 74 + }; 84 75 85 - for (const trackId of trackIds) { 86 - const trackLikes = likes.filter( 87 - (l) => l.loved_tracks.trackId === trackId, 88 - ); 89 - likesMap.set(trackId, { 90 - count: trackLikes.length, 91 - liked: trackLikes.some((l) => l.users.did === params.did), 92 - }); 93 - } 76 + const fetchScrobbles = async ( 77 + ctx: Context, 78 + params: QueryParams, 79 + filterDids: string[] | null, 80 + ) => { 81 + const baseQuery = ctx.db 82 + .select() 83 + .from(tables.scrobbles) 84 + .leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id)) 85 + .leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id)); 94 86 95 - return scrobbles.map((row) => ({ 96 - ...row, 97 - likesCount: likesMap.get(row.tracks?.id)?.count ?? 0, 98 - liked: likesMap.get(row.tracks?.id)?.liked ?? false, 99 - })); 100 - } else { 101 - return []; 102 - } 103 - } 87 + const query = filterDids 88 + ? baseQuery.where(inArray(tables.users.did, filterDids)) 89 + : baseQuery; 104 90 105 - const scrobbles = await baseQuery 106 - .orderBy(desc(tables.scrobbles.timestamp)) 107 - .offset(params.offset || 0) 108 - .limit(params.limit || 20) 109 - .execute(); 91 + return query 92 + .orderBy(desc(tables.scrobbles.timestamp)) 93 + .offset(params.offset || 0) 94 + .limit(params.limit || 20) 95 + .execute(); 96 + }; 110 97 111 - const trackIds = scrobbles.map((row) => row.tracks?.id).filter(Boolean); 98 + const enrichWithLikes = async ( 99 + ctx: Context, 100 + scrobbles: Awaited<ReturnType<typeof fetchScrobbles>>, 101 + currentUserDid?: string, 102 + ) => { 103 + const trackIds = scrobbles 104 + .map((row) => row.tracks?.id) 105 + .filter((id): id is string => Boolean(id)); 112 106 113 - const likes = await ctx.db 114 - .select() 115 - .from(tables.lovedTracks) 116 - .leftJoin(tables.users, eq(tables.lovedTracks.userId, tables.users.id)) 117 - .where(inArray(tables.lovedTracks.trackId, trackIds)) 118 - .execute(); 107 + if (trackIds.length === 0) { 108 + return scrobbles.map((row) => ({ ...row, likesCount: 0, liked: false })); 109 + } 119 110 120 - const likesMap = new Map<string, { count: number; liked: boolean }>(); 111 + const likes = await ctx.db 112 + .select() 113 + .from(tables.lovedTracks) 114 + .leftJoin(tables.users, eq(tables.lovedTracks.userId, tables.users.id)) 115 + .where(inArray(tables.lovedTracks.trackId, trackIds)) 116 + .execute(); 121 117 122 - for (const trackId of trackIds) { 123 - const trackLikes = likes.filter( 124 - (l) => l.loved_tracks.trackId === trackId, 125 - ); 126 - likesMap.set(trackId, { 127 - count: trackLikes.length, 128 - liked: trackLikes.some((l) => l.users.did === params.did), 129 - }); 130 - } 118 + const likesMap = new Map<string, { count: number; liked: boolean }>(); 131 119 132 - return scrobbles.map((row) => ({ 133 - ...row, 134 - likesCount: likesMap.get(row.tracks?.id)?.count ?? 0, 135 - liked: likesMap.get(row.tracks?.id)?.liked ?? false, 136 - })); 137 - }, 120 + for (const trackId of trackIds) { 121 + const trackLikes = likes.filter( 122 + (l) => l.loved_tracks.trackId === trackId, 123 + ); 124 + likesMap.set(trackId, { 125 + count: trackLikes.length, 126 + liked: trackLikes.some((l) => l.users?.did === currentUserDid), 127 + }); 128 + } 138 129 139 - catch: (error) => new Error(`Failed to retrieve scrobbles: ${error}`), 140 - }); 130 + return scrobbles.map((row) => ({ 131 + ...row, 132 + likesCount: likesMap.get(row.tracks?.id ?? "")?.count ?? 0, 133 + liked: likesMap.get(row.tracks?.id ?? "")?.liked ?? false, 134 + })); 141 135 }; 142 136 143 137 const presentation = ( ··· 155 149 tags: [], 156 150 id: scrobbles.id, 157 151 trackUri: tracks.uri, 158 - likesCount: likesCount, 159 - liked: liked, 152 + likesCount, 153 + liked, 160 154 })), 161 155 })); 162 156 };