A decentralized music tracking and discovery platform built on AT Protocol 🎵

[api] auto sync meilisearch on new track, artist, album and user

+159 -91
+4
apps/api/src/subscribers/index.ts
··· 1 1 import { Context } from "context"; 2 2 import { onNewPlaylist } from "./playlist"; 3 + import { onNewTrack } from "./track"; 4 + import { onNewUser } from "./user"; 3 5 4 6 export default function subscribe(ctx: Context) { 5 7 onNewPlaylist(ctx); 8 + onNewTrack(ctx); 9 + onNewUser(ctx); 6 10 }
+51
apps/api/src/subscribers/track.ts
··· 1 + import chalk from "chalk"; 2 + import { Context } from "context"; 3 + import { eq } from "drizzle-orm"; 4 + import _ from "lodash"; 5 + import { StringCodec } from "nats"; 6 + import tables from "schema"; 7 + 8 + export function onNewTrack(ctx: Context) { 9 + const sc = StringCodec(); 10 + const sub = ctx.nc.subscribe("rocksky.track"); 11 + (async () => { 12 + for await (const m of sub) { 13 + const payload: { 14 + track: { xata_id: string }; 15 + artist_album: { 16 + artist_id: { xata_id: string }; 17 + album_id: { xata_id: string }; 18 + }; 19 + } = JSON.parse(sc.decode(m.data)); 20 + 21 + const [tracks, artists, albums] = await Promise.all([ 22 + ctx.db 23 + .select() 24 + .from(tables.tracks) 25 + .where(eq(tables.tracks.id, payload.track.xata_id)) 26 + .execute(), 27 + ctx.db 28 + .select() 29 + .from(tables.artists) 30 + .where(eq(tables.artists.id, payload.artist_album.artist_id.xata_id)) 31 + .execute(), 32 + ctx.db 33 + .select() 34 + .from(tables.albums) 35 + .where(eq(tables.albums.id, payload.artist_album.album_id.xata_id)) 36 + .execute(), 37 + ]); 38 + 39 + console.log(`New track: ${chalk.cyan(_.get(tracks, "0.title"))}`); 40 + 41 + await Promise.all([ 42 + ctx.meilisearch.post(`indexes/albums/documents?primaryKey=id`, albums), 43 + ctx.meilisearch.post( 44 + `indexes/artists/documents?primaryKey=id`, 45 + artists 46 + ), 47 + ctx.meilisearch.post(`indexes/tracks/documents?primaryKey=id`, tracks), 48 + ]); 49 + } 50 + })(); 51 + }
+30
apps/api/src/subscribers/user.ts
··· 1 + import chalk from "chalk"; 2 + import { Context } from "context"; 3 + import { eq } from "drizzle-orm"; 4 + import _ from "lodash"; 5 + import { StringCodec } from "nats"; 6 + import tables from "schema"; 7 + 8 + export function onNewUser(ctx: Context) { 9 + const sc = StringCodec(); 10 + const sub = ctx.nc.subscribe("rocksky.user"); 11 + (async () => { 12 + for await (const m of sub) { 13 + const payload: { 14 + xata_id: string; 15 + } = JSON.parse(sc.decode(m.data)); 16 + const results = await ctx.db 17 + .select() 18 + .from(tables.users) 19 + .where(eq(tables.users.id, payload.xata_id)) 20 + .execute(); 21 + 22 + console.log(`New user: ${chalk.cyan(_.get(results, "0.handle"))}`); 23 + 24 + await ctx.meilisearch.post( 25 + `/indexes/users/documents?primaryKey=id`, 26 + results 27 + ); 28 + } 29 + })(); 30 + }
-13
apps/web/src/hooks/useSearch.tsx
··· 1 1 import { useMutation } from "@tanstack/react-query"; 2 - import axios from "axios"; 3 2 import { search } from "../api/search"; 4 - import { API_URL } from "../consts"; 5 3 6 4 export const useSearchMutation = () => 7 5 useMutation({ 8 6 mutationFn: (query: string) => search(query), 9 7 }); 10 - 11 - function useSearch() { 12 - const search = async (query: string) => { 13 - const response = await axios.get(`${API_URL}/search?q=${query}&size=100`); 14 - return response.data; 15 - }; 16 - 17 - return { search }; 18 - } 19 - 20 - export default useSearch;
+74 -78
apps/web/src/layouts/Search/Search.tsx
··· 5 5 import { Input } from "baseui/input"; 6 6 import { PLACEMENT, Popover } from "baseui/popover"; 7 7 import _ from "lodash"; 8 - import { useCallback, useEffect, useState } from "react"; 8 + import { useEffect, useState } from "react"; 9 9 import { Controller, useForm } from "react-hook-form"; 10 10 import { Link as DefaultLink } from "react-router"; 11 11 import z from "zod"; 12 12 import Artist from "../../components/Icons/Artist"; 13 13 import Disc from "../../components/Icons/Disc"; 14 14 import Track from "../../components/Icons/Track"; 15 - import useSearch, { useSearchMutation } from "../../hooks/useSearch"; 15 + import { useSearchMutation } from "../../hooks/useSearch"; 16 16 17 17 const Link = styled(DefaultLink)` 18 18 color: initial; ··· 28 28 }); 29 29 30 30 function Search() { 31 - const [results, setResults] = useState([]); 32 - 31 + const [results, setResults] = useState<any[]>([]); 33 32 const { mutate, data } = useSearchMutation(); 34 33 35 - const { search } = useSearch(); 36 34 const { 37 35 control, 38 36 formState: { errors }, ··· 47 45 48 46 const keyword = watch("keyword"); 49 47 50 - // eslint-disable-next-line react-hooks/exhaustive-deps 51 - const debouncedSearch = useCallback( 52 - _.debounce(async (keyword) => { 53 - mutate(keyword); 54 - const data = await search(keyword); 55 - setResults(data.records); 56 - }, 300), 57 - [search] 58 - ); 48 + const debouncedSearch = _.debounce(async (keyword) => { 49 + mutate(keyword); 50 + }, 200); 59 51 60 52 useEffect(() => { 61 53 if (keyword.length === 0) { ··· 66 58 // eslint-disable-next-line react-hooks/exhaustive-deps 67 59 }, [keyword]); 68 60 69 - console.log(">> data", data); 61 + useEffect(() => { 62 + if (data && data.hits) { 63 + setResults(data.hits); 64 + } else { 65 + setResults([]); 66 + } 67 + }, [data]); 70 68 71 69 return ( 72 70 <> ··· 83 81 <> 84 82 {results.map((item: any) => ( 85 83 <> 86 - {item.table === "users" && ( 87 - <Link 88 - to={`/profile/${item.record.handle}`} 89 - key={item.record.xata_id} 90 - > 84 + {item._federation.indexUid === "users" && ( 85 + <Link to={`/profile/${item.handle}`} key={item.id}> 91 86 <div className="flex flex-row mb-[10px]"> 92 87 <img 93 - key={item.record.did} 94 - src={item.record.avatar} 95 - alt={item.record.display_name} 88 + key={item.did} 89 + src={item.avatar} 90 + alt={item.displayName} 96 91 className="w-[50px] h-[50px] mr-[12px] rounded-full" 97 92 /> 98 93 <div> 99 94 <div className="overflow-hidden"> 100 95 <div className="overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text)]"> 101 - {item.record.display_name} 96 + {item.displayName} 102 97 </div> 103 98 </div> 104 99 <div className="text-[var(--color-text-muted)] text-[14px]"> 105 - @{item.record.handle} 100 + @{item.handle} 106 101 </div> 107 102 </div> 108 103 </div> 109 104 </Link> 110 105 )} 111 106 112 - {item.record.uri && 113 - (item.record.name || item.record.title) && 114 - item.record.type !== "users" && ( 107 + {item.uri && 108 + (item.name || item.title) && 109 + item._federation.indexUid !== "users" && ( 115 110 <Link 116 - to={`/${item.record.uri?.split("at://")[1]}`} 117 - key={item.record.xata_id} 111 + to={`/${item.uri?.split("at://")[1]}`} 112 + key={item.id} 118 113 > 119 114 <div 120 - key={item.record.xata_id} 115 + key={item.id} 121 116 className="h-[64px] flex flex-row items-center" 122 117 > 123 - {item.table === "artists" && 124 - item.record.picture && ( 118 + {item._federation.indexUid === "artists" && 119 + item.picture && ( 125 120 <img 126 - key={item.record.xata_id} 127 - src={item.record.picture} 128 - alt={item.record.name} 121 + key={item.id} 122 + src={item.picture} 123 + alt={item.name} 129 124 className="w-[50px] h-[50px] mr-[12px] rounded-full" 130 125 /> 131 126 )} 132 - {item.table === "artists" && 133 - !item.record.picture && ( 127 + {item._federation.indexUid === "artists" && 128 + !item.picture && ( 134 129 <div 135 - key={item.record.xata_id} 130 + key={item.id} 136 131 className="w-[50px] h-[50px] mr-[12px] rounded-full flex items-center bg-[rgba(243, 243, 243, 0.725)]" 137 132 > 138 133 <div className="w-[28px] h-[28px]"> ··· 140 135 </div> 141 136 </div> 142 137 )} 143 - {item.table === "albums" && 144 - item.record.album_art && ( 138 + {item._federation.indexUid === "albums" && 139 + item.albumArt && ( 145 140 <img 146 - key={item.record.xata_id} 147 - src={item.record.album_art} 148 - alt={item.record.title} 141 + key={item.id} 142 + src={item.albumArt} 143 + alt={item.title} 149 144 className="w-[50px] h-[50px] mr-[12px]" 150 145 /> 151 146 )} 152 - {item.table === "albums" && 153 - !item.record.album_art && ( 147 + {item._federation.indexUid === "albums" && 148 + !item.albumArt && ( 154 149 <div className="w-[50px] h-[50px] mr-[12px] rounded-full flex items-center bg-[rgba(243, 243, 243, 0.725)]"> 155 150 <div className="w-[28px] h-[28px]"> 156 151 <Disc ··· 161 156 </div> 162 157 </div> 163 158 )} 164 - {item.table === "tracks" && 165 - item.record.album_art && ( 159 + {item._federation.indexUid === "tracks" && 160 + item.albumArt && ( 166 161 <img 167 - key={item.record.xata_id} 168 - src={item.record.album_art} 169 - alt={item.record.title} 162 + key={item.id} 163 + src={item.albumArt} 164 + alt={item.title} 170 165 className="w-[50px] h-[50px] mr-[12px]" 171 166 /> 172 167 )} 173 - {item.table === "tracks" && 174 - !item.record.album_art && ( 168 + {item._federation.indexUid === "tracks" && 169 + !item.albumArt && ( 175 170 <div className="w-[50px] h-[50px] mr-[12px] rounded-full flex items-center bg-[rgba(243, 243, 243, 0.725)]"> 176 171 <div className="w-[28px] h-[28px]"> 177 172 <Track color="rgba(66, 87, 108, 0.65)" /> ··· 180 175 )} 181 176 <div className="overflow-hidden w-[calc(100%-70px)]"> 182 177 <div className="overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text)]"> 183 - {item.record.name || item.record.title} 178 + {item.name || item.title} 184 179 </div> 185 - {item.table === "tracks" && ( 180 + {item._federation.indexUid === "tracks" && ( 186 181 <div className="text-[14px] text-[var(--color-text-muted)]"> 187 182 Track 188 183 </div> 189 184 )} 190 - {item.table === "albums" && ( 185 + {item._federation.indexUid === "albums" && ( 191 186 <div className="text-[14px] text-[var(--color-text-muted)]"> 192 187 Album 193 188 </div> 194 189 )} 195 - {item.table === "artists" && ( 190 + {item._federation.indexUid === "artists" && ( 196 191 <div className="text-[var(--color-text-muted)] text-[14px]"> 197 192 Artist 198 193 </div> ··· 201 196 </div> 202 197 </Link> 203 198 )} 204 - {!item.record.uri && 205 - (item.record.name || item.record.title) && 206 - item.tables !== "users" && ( 199 + {!item.uri && 200 + (item.name || item.title) && 201 + item._federation.indexUid !== "users" && ( 207 202 <div> 208 203 <div 209 204 key={item.id} 210 205 className="h-[64px] flex flex-row items-center" 211 206 > 212 - {item.table === "artists" && 213 - item.record.picture && ( 207 + {item._federation.indexUid === "artists" && 208 + item.picture && ( 214 209 <img 215 - src={item.record.picture} 216 - alt={item.record.name} 210 + src={item.picture} 211 + alt={item.name} 217 212 className="w-[50px] h-[50px] mr-[12px] rounded-full" 218 213 /> 219 214 )} 220 - {item.table === "artists" && 221 - !item.record.picture && ( 215 + {item._federation.indexUid === "artists" && 216 + !item.picture && ( 222 217 <div className="w-[50px] h-[50px] mr-[12px] rounded-full flex items-center bg-[rgba(243, 243, 243, 0.725)]"> 223 218 <div className="w-[28px] h-[28px]"> 224 219 <Artist color="rgba(66, 87, 108, 0.65)" /> 225 220 </div> 226 221 </div> 227 222 )} 228 - {item.table === "albums" && 229 - item.record.album_art && ( 223 + {item._federation.indexUid === "albums" && 224 + item.albumArt && ( 230 225 <img 231 - src={item.record.album_art} 232 - alt={item.record.title} 226 + src={item.albumArt} 227 + alt={item.title} 233 228 className="w-[50px] h-[50px] mr-[12px]" 234 229 /> 235 230 )} 236 - {item.table === "tracks" && ( 231 + {item._federation.indexUid === "tracks" && ( 237 232 <img 238 - src={item.record.album_art} 239 - alt={item.record.title} 233 + src={item.albumArt} 234 + alt={item.title} 240 235 className="w-[50px] h-[50px] mr-[12px]" 241 236 /> 242 237 )} 243 238 {["artists", "albums", "tracks"].includes( 244 - item.table 239 + item._federation.indexUid 245 240 ) && ( 246 241 <div className="overflow-hidden"> 247 242 <div className="overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text)]"> 248 - {item.record.name || item.record.title} 243 + {item.name || item.title} 249 244 </div> 250 - {item.table === "tracks" && ( 245 + {item._federation.indexUid === "tracks" && ( 251 246 <div className="text-[14px] text-[var(--color-text-muted)]"> 252 247 Track 253 248 </div> 254 249 )} 255 - {item.table === "albums" && ( 250 + {item._federation.indexUid === "albums" && ( 256 251 <div className="text-[14px] text-[var(--color-text-muted)]"> 257 252 Album 258 253 </div> 259 254 )} 260 - {item.table === "artists" && ( 255 + {item._federation.indexUid === 256 + "artists" && ( 261 257 <div className="text-[14px] text-[var(--color-text-muted)]"> 262 258 Artist 263 259 </div>