import { createStore } from 'solid-js/store'; import { createMemo } from 'solid-js'; import { LRCLib } from './LRCLib'; import { LyricsGenius } from './LyricsGenius'; import { MusixMatch } from './MusixMatch'; import { YTMusic } from './YTMusic'; import { getSongInfo } from '@/providers/song-info-front'; import type { LyricProvider, LyricResult } from '../types'; import type { SongInfo } from '@/providers/song-info'; export const providers = { YTMusic: new YTMusic(), LRCLib: new LRCLib(), MusixMatch: new MusixMatch(), LyricsGenius: new LyricsGenius(), // Megalobiz: new Megalobiz(), // Disabled because it is too unstable and slow } as const; export type ProviderName = keyof typeof providers; export const providerNames = Object.keys(providers) as ProviderName[]; export type ProviderState = { state: 'fetching' | 'done' | 'error'; data: LyricResult | null; error: Error | null; }; type LyricsStore = { provider: ProviderName; current: ProviderState; lyrics: Record; }; const initialData = () => providerNames.reduce( (acc, name) => { acc[name] = { state: 'fetching', data: null, error: null }; return acc; }, {} as LyricsStore['lyrics'], ); export const [lyricsStore, setLyricsStore] = createStore({ provider: providerNames[0], lyrics: initialData(), get current(): ProviderState { return this.lyrics[this.provider]; }, }); export const currentLyrics = createMemo(() => { const provider = lyricsStore.provider; return lyricsStore.lyrics[provider]; }); type VideoId = string; type SearchCacheData = Record; interface SearchCache { state: 'loading' | 'done'; data: SearchCacheData; } // TODO: Maybe use localStorage for the cache. const searchCache = new Map(); export const fetchLyrics = (info: SongInfo) => { if (searchCache.has(info.videoId)) { const cache = searchCache.get(info.videoId)!; if (cache.state === 'loading') { setTimeout(() => { fetchLyrics(info); }); return; } if (getSongInfo().videoId === info.videoId) { setLyricsStore('lyrics', () => { // weird bug with solid-js return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data; }); } return; } const cache: SearchCache = { state: 'loading', data: initialData(), }; searchCache.set(info.videoId, cache); if (getSongInfo().videoId === info.videoId) { setLyricsStore('lyrics', () => { // weird bug with solid-js return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data; }); } const tasks: Promise[] = []; // prettier-ignore for ( const [providerName, provider] of Object.entries(providers) as [ ProviderName, LyricProvider, ][] ) { const pCache = cache.data[providerName]; tasks.push( provider .search(info) .then((res) => { pCache.state = 'done'; pCache.data = res; if (getSongInfo().videoId === info.videoId) { setLyricsStore('lyrics', (old) => { return { ...old, [providerName]: { state: 'done', data: res ? { ...res } : null, error: null, }, }; }); } }) .catch((error: Error) => { pCache.state = 'error'; pCache.error = error; console.error(error); if (getSongInfo().videoId === info.videoId) { setLyricsStore('lyrics', (old) => { return { ...old, [providerName]: { state: 'error', error, data: null }, }; }); } }), ); } Promise.allSettled(tasks).then(() => { cache.state = 'done'; searchCache.set(info.videoId, cache); }); }; export const retrySearch = (provider: ProviderName, info: SongInfo) => { setLyricsStore('lyrics', (old) => { const pCache = { state: 'fetching', data: null, error: null, }; return { ...old, [provider]: pCache, }; }); providers[provider] .search(info) .then((res) => { setLyricsStore('lyrics', (old) => { return { ...old, [provider]: { state: 'done', data: res, error: null }, }; }); }) .catch((error) => { setLyricsStore('lyrics', (old) => { return { ...old, [provider]: { state: 'error', data: null, error }, }; }); }); };