useGunDBSearch.ts
1 import { useState, useCallback } from 'react' 2 import { gunDBService, type GunDBSong } from '../services/database/gundb' 3 import { litProtocolService } from '../lib/litProtocol' 4 import { useLitSession } from './useLitSession' 5 import { optimisticCache } from '../services/database/gundb/optimisticCache' 6 import { isPopularSong, getPopularSongData } from '../constants/popularSongs' 7 import { useAccount } from 'wagmi' 8 9 // Lit Action URLs will be loaded dynamically 10 11 // Define the search result type based on what Genius API returns 12 interface SearchResult { 13 genius_id: number 14 title: string 15 title_with_featured?: string 16 artist: string 17 artist_id?: number 18 genius_slug: string 19 url: string 20 artwork_thumbnail?: string 21 lyrics_state?: string 22 _score?: number 23 } 24 25 interface UseGunDBSearchReturn { 26 searchSongs: (query: string) => Promise<SearchResult[]> 27 getSongMetadata: (geniusId: number) => Promise<GunDBSong | null> 28 isSearching: boolean 29 isFetchingSong: boolean 30 error: string | null 31 } 32 33 export function useGunDBSearch(): UseGunDBSearchReturn { 34 const [isSearching, setIsSearching] = useState(false) 35 const [isFetchingSong, setIsFetchingSong] = useState(false) 36 const [error, setError] = useState<string | null>(null) 37 const { sessionSigs, createSession } = useLitSession() 38 const { address } = useAccount() 39 40 const searchSongs = useCallback(async (query: string): Promise<SearchResult[]> => { 41 setIsSearching(true) 42 setError(null) 43 44 try { 45 // Fetch user's public IP for analytics 46 let userIp = 'unknown'; 47 try { 48 const ipResponse = await fetch('https://api.ipify.org?format=json'); 49 if (ipResponse.ok) { 50 const ipData = await ipResponse.json(); 51 userIp = ipData.ip; 52 console.log('🌐 User IP for analytics:', userIp); 53 } 54 } catch (ipError) { 55 console.warn('Failed to fetch user IP:', ipError); 56 } 57 58 // Get user agent 59 const userAgent = navigator.userAgent; 60 const cacheKey = `search:${query.toLowerCase().trim()}` 61 62 const results = await optimisticCache.getOrExecute( 63 cacheKey, 64 async () => { 65 // Always return null to force fresh search 66 return null 67 }, 68 async () => { 69 // Execute Lit Action 70 console.log('📡 Executing Genius search Lit Action...') 71 console.log('👤 Wallet address:', address || 'not connected') 72 73 let currentSessionSigs = sessionSigs 74 if (!currentSessionSigs) { 75 console.log('🔐 Creating new Lit session...') 76 currentSessionSigs = await createSession() 77 if (!currentSessionSigs) { 78 throw new Error('Failed to create Lit session') 79 } 80 } 81 82 // Load the Lit Action code 83 const baseUrl = window.location.origin + window.location.pathname 84 const litActionUrl = new URL('./lit-actions/genius-search/geniusSearch.js', baseUrl).href 85 const geniusSearchCode = await fetch(litActionUrl).then(r => r.text()) 86 87 const result = await litProtocolService.litNodeClient!.executeJs({ 88 code: geniusSearchCode, 89 sessionSigs: currentSessionSigs, 90 jsParams: { 91 query, 92 limit: 15, 93 walletAddress: address || undefined, 94 userIp: userIp, 95 userAgent: userAgent 96 } 97 }) as any 98 99 // Log the full result including logs from Lit Action 100 console.log('📡 Lit Action full result:', result) 101 102 // Extract and display logs if available 103 if ('logs' in result && result.logs) { 104 console.log('📡 Lit Action logs:', result.logs) 105 // Check if logs is a string or array 106 if (typeof result.logs === 'string') { 107 console.log(' ', result.logs) 108 } else if (Array.isArray(result.logs)) { 109 result.logs.forEach((log: any) => console.log(' ', log)) 110 } 111 } 112 113 const response = JSON.parse(result.response as string) 114 115 // Log analytics info if available 116 if (response.analytics) { 117 console.log('📊 Analytics info from Lit Action:', response.analytics) 118 } 119 120 if (!response.success) { 121 throw new Error(response.error || 'Search failed') 122 } 123 124 // Don't cache search results to prevent spoofing 125 return response.results 126 } 127 ) 128 129 return results || [] 130 } catch (err) { 131 const errorMessage = err instanceof Error ? err.message : 'Search failed' 132 console.error('❌ Search error:', errorMessage) 133 setError(errorMessage) 134 return [] 135 } finally { 136 setIsSearching(false) 137 } 138 }, [sessionSigs, createSession, address]) 139 140 const getSongMetadata = useCallback(async (geniusId: number): Promise<GunDBSong | null> => { 141 setIsFetchingSong(true) 142 setError(null) 143 144 try { 145 // Check if this is a popular song with pre-populated data 146 if (isPopularSong(geniusId)) { 147 console.log('🌟 Using pre-populated data for popular song:', geniusId) 148 const popularData = getPopularSongData(geniusId) 149 if (popularData) { 150 setIsFetchingSong(false) 151 return popularData.song 152 } 153 } 154 155 const cacheKey = `song:${geniusId}` 156 157 const song = await optimisticCache.getOrExecute( 158 cacheKey, 159 async () => { 160 // Check GunDB cache 161 console.log('🔍 Checking GunDB for song:', geniusId) 162 const cachedSong = await gunDBService.getSongByGeniusId(geniusId) 163 if (cachedSong) { 164 console.log('✅ Found song in GunDB:', cachedSong.title) 165 return cachedSong 166 } 167 return null 168 }, 169 async () => { 170 // Execute Lit Action 171 console.log('📡 Executing Genius song fetcher Lit Action...') 172 let currentSessionSigs = sessionSigs 173 if (!currentSessionSigs) { 174 console.log('🔐 Creating new Lit session...') 175 currentSessionSigs = await createSession() 176 if (!currentSessionSigs) { 177 throw new Error('Failed to create Lit session') 178 } 179 } 180 181 // Load the Lit Action code 182 const baseUrl = window.location.origin + window.location.pathname 183 const litActionUrl = new URL('./lit-actions/genius-song-fetcher/geniusSongFetcher.js', baseUrl).href 184 const geniusSongFetcherCode = await fetch(litActionUrl).then(r => r.text()) 185 186 const result = await litProtocolService.litNodeClient!.executeJs({ 187 code: geniusSongFetcherCode, 188 sessionSigs: currentSessionSigs, 189 jsParams: { 190 songId: geniusId.toString() 191 } 192 }) 193 194 const response = JSON.parse(result.response as string) 195 196 if (!response.success || !response.metadata) { 197 throw new Error(response.error || 'Failed to fetch song metadata') 198 } 199 200 // Convert to GunDB format and save 201 const gunDBSong: GunDBSong = { 202 genius_id: response.metadata.genius_id, 203 title: response.metadata.title, 204 artist: response.metadata.artist, 205 genius_slug: response.metadata.genius_slug, 206 streaming_links: response.metadata.streaming_links, 207 artwork_thumbnail: response.metadata.artwork_hash ? 208 `https://images.genius.com/${response.metadata.artwork_hash.id}.${response.metadata.artwork_hash.sizes.t}.${response.metadata.artwork_hash.ext}` : 209 undefined, 210 created_at: Date.now() 211 } 212 213 console.log('💾 Saving song to GunDB:', gunDBSong.title) 214 await gunDBService.saveSong(gunDBSong) 215 216 return gunDBSong 217 } 218 ) 219 220 return song 221 } catch (err) { 222 const errorMessage = err instanceof Error ? err.message : 'Failed to fetch song' 223 console.error('❌ Fetch song error:', errorMessage) 224 setError(errorMessage) 225 return null 226 } finally { 227 setIsFetchingSong(false) 228 } 229 }, [sessionSigs, createSession]) 230 231 return { 232 searchSongs, 233 getSongMetadata, 234 isSearching, 235 isFetchingSong, 236 error 237 } 238 }