/ apps / web / src / hooks / useGunDBSearch.ts
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  }