import { UseQueryResult, useQuery, useQueryClient } from '@tanstack/react-query'; import { LyricsQuery, QueueSong, SynchronizedLyricsArray, InternetProviderLyricResponse, FullLyricsMetadata, LyricGetQuery, StructuredLyric, ServerType, } from '/@/renderer/api/types'; import { QueryHookArgs } from '/@/renderer/lib/react-query'; import { getServerById, useLyricsSettings } from '/@/renderer/store'; import { queryKeys } from '/@/renderer/api/query-keys'; import { api } from '/@/renderer/api'; import isElectron from 'is-electron'; import { hasFeature } from '/@/renderer/api/utils'; import { ServerFeature } from '/@/renderer/api/features.types'; const lyricsIpc = isElectron() ? window.electron.lyrics : null; // Match LRC lyrics format by https://github.com/ustbhuangyi/lyric-parser // [mm:ss.SSS] text const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]([^\n]+)(\n|$)/g; // Match karaoke lyrics format returned by NetEase // [SSS,???] text const alternateTimeExp = /\[(\d*),(\d*)]([^\n]+)(\n|$)/g; const formatLyrics = (lyrics: string) => { const synchronizedLines = lyrics.matchAll(timeExp); const formattedLyrics: SynchronizedLyricsArray = []; for (const line of synchronizedLines) { const [, minute, sec, ms, text] = line; const minutes = parseInt(minute, 10); const seconds = parseInt(sec, 10); const milis = ms?.length === 3 ? parseInt(ms, 10) : parseInt(ms, 10) * 10; const timeInMilis = (minutes * 60 + seconds) * 1000 + milis; formattedLyrics.push([timeInMilis, text]); } if (formattedLyrics.length > 0) return formattedLyrics; const alternateSynchronizedLines = lyrics.matchAll(alternateTimeExp); for (const line of alternateSynchronizedLines) { const [, timeInMilis, , text] = line; const cleanText = text .replaceAll(/\(\d+,\d+\)/g, '') .replaceAll(/\s,/g, ',') .replaceAll(/\s\./g, '.'); formattedLyrics.push([Number(timeInMilis), cleanText]); } if (formattedLyrics.length > 0) return formattedLyrics; // If no synchronized lyrics were found, return the original lyrics return lyrics; }; export const useServerLyrics = ( args: QueryHookArgs, ): UseQueryResult => { const { query, serverId } = args; const server = getServerById(serverId); return useQuery({ // Note: This currently fetches for every song, even if it shouldn't have // lyrics, because for some reason HasLyrics is not exposed. Thus, ignore the error onError: () => {}, queryFn: ({ signal }) => { if (!server) throw new Error('Server not found'); // This should only be called for Jellyfin. Return null to ignore errors if (server.type !== ServerType.JELLYFIN) return null; return api.controller.getLyrics({ apiClientProps: { server, signal }, query }); }, queryKey: queryKeys.songs.lyrics(server?.id || '', query), }); }; export const useSongLyricsBySong = ( args: QueryHookArgs, song: QueueSong | undefined, ): UseQueryResult => { const { query } = args; const { fetch } = useLyricsSettings(); const server = getServerById(song?.serverId); return useQuery({ cacheTime: Infinity, enabled: !!song && !!server, onError: () => {}, queryFn: async ({ signal }): Promise => { if (!server) throw new Error('Server not found'); if (!song) return null; if (server.type === ServerType.JELLYFIN) { const jfLyrics = await api.controller .getLyrics({ apiClientProps: { server, signal }, query: { songId: song.id }, }) .catch((err) => console.log(err)); if (jfLyrics) { return { artist: song.artists?.[0]?.name, lyrics: jfLyrics, name: song.name, remote: false, source: server?.name ?? 'music server', }; } } else if (hasFeature(server, ServerFeature.SONG_LYRICS)) { const subsonicLyrics = await api.controller .getStructuredLyrics({ apiClientProps: { server, signal }, query: { songId: song.id }, }) .catch(console.error); if (subsonicLyrics) { return subsonicLyrics; } } else if (song.lyrics) { return { artist: song.artists?.[0]?.name, lyrics: formatLyrics(song.lyrics), name: song.name, remote: false, source: server?.name ?? 'music server', }; } if (fetch) { const remoteLyricsResult: InternetProviderLyricResponse | null = await lyricsIpc?.getRemoteLyricsBySong(song); if (remoteLyricsResult) { return { ...remoteLyricsResult, lyrics: formatLyrics(remoteLyricsResult.lyrics), remote: true, }; } } return null; }, queryKey: queryKeys.songs.lyrics(server?.id || '', query), staleTime: Infinity, }); }; export const useSongLyricsByRemoteId = ( args: QueryHookArgs>, ): UseQueryResult => { const queryClient = useQueryClient(); const { query, serverId } = args; return useQuery({ enabled: !!query.remoteSongId && !!query.remoteSource, onError: () => {}, onSuccess: (data) => { if (!data || !query.song) { return; } const lyricsResult = { artist: query.song.artists?.[0]?.name, lyrics: data, name: query.song.name, remote: false, source: query.remoteSource, }; queryClient.setQueryData( queryKeys.songs.lyrics(serverId, { songId: query.song.id }), lyricsResult, ); }, queryFn: async () => { const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId( query as LyricGetQuery, ); if (remoteLyricsResult) { return formatLyrics(remoteLyricsResult); } return null; }, queryKey: queryKeys.songs.lyricsByRemoteId(query), }); };