mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-15 13:00:25 +02:00
upgrade and refactor for react-query v5
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById, useSettingsStore } from '/@/renderer/store';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import {
|
||||
FullLyricsMetadata,
|
||||
InternetProviderLyricResponse,
|
||||
InternetProviderLyricSearchResponse,
|
||||
LyricGetQuery,
|
||||
LyricSearchQuery,
|
||||
LyricsQuery,
|
||||
QueueSong,
|
||||
ServerType,
|
||||
StructuredLyric,
|
||||
SynchronizedLyricsArray,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { LyricSource } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
const lyricsIpc = isElectron() ? window.api.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 lyricsQueries = {
|
||||
search: (args: Omit<QueryHookArgs<LyricSearchQuery>, 'serverId'>) => {
|
||||
return queryOptions({
|
||||
gcTime: 1000 * 60 * 1,
|
||||
queryFn: () => {
|
||||
if (lyricsIpc) {
|
||||
return lyricsIpc.searchRemoteLyrics(args.query);
|
||||
}
|
||||
return {} as Record<LyricSource, InternetProviderLyricSearchResponse[]>;
|
||||
},
|
||||
queryKey: queryKeys.songs.lyricsSearch(args.query),
|
||||
staleTime: 1000 * 60 * 1,
|
||||
...args.options,
|
||||
});
|
||||
},
|
||||
serverLyrics: (args: QueryHookArgs<LyricsQuery>) => {
|
||||
return queryOptions({
|
||||
queryFn: ({ signal }) => {
|
||||
const server = getServerById(args.serverId);
|
||||
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: args.query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.songs.lyrics(args.serverId || '', args.query),
|
||||
...args.options,
|
||||
});
|
||||
},
|
||||
songLyrics: (args: QueryHookArgs<LyricsQuery>, song: QueueSong | undefined) => {
|
||||
return queryOptions({
|
||||
gcTime: Infinity,
|
||||
queryFn: async ({ signal }): Promise<FullLyricsMetadata | null | StructuredLyric[]> => {
|
||||
const server = getServerById(song?.serverId);
|
||||
if (!server) throw new Error('Server not found');
|
||||
if (!song) return null;
|
||||
|
||||
const { preferLocalLyrics } = useSettingsStore.getState().lyrics;
|
||||
|
||||
let localLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
|
||||
let remoteLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
|
||||
|
||||
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
|
||||
const subsonicLyrics = await api.controller
|
||||
.getStructuredLyrics({
|
||||
apiClientProps: { server, signal },
|
||||
query: { songId: song.id },
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
if (subsonicLyrics?.length) {
|
||||
localLyrics = subsonicLyrics;
|
||||
}
|
||||
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
|
||||
const jfLyrics = await api.controller
|
||||
.getLyrics({
|
||||
apiClientProps: { server, signal },
|
||||
query: { songId: song.id },
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
if (jfLyrics) {
|
||||
localLyrics = {
|
||||
artist: song.artists?.[0]?.name,
|
||||
lyrics: jfLyrics,
|
||||
name: song.name,
|
||||
remote: false,
|
||||
source: server?.name ?? 'music server',
|
||||
};
|
||||
}
|
||||
} else if (song.lyrics) {
|
||||
localLyrics = {
|
||||
artist: song.artists?.[0]?.name,
|
||||
lyrics: formatLyrics(song.lyrics),
|
||||
name: song.name,
|
||||
remote: false,
|
||||
source: server?.name ?? 'music server',
|
||||
};
|
||||
}
|
||||
|
||||
if (preferLocalLyrics && localLyrics) {
|
||||
return localLyrics;
|
||||
}
|
||||
|
||||
const { fetch } = useSettingsStore.getState().lyrics;
|
||||
|
||||
if (fetch) {
|
||||
const remoteLyricsResult: InternetProviderLyricResponse | null =
|
||||
await lyricsIpc?.getRemoteLyricsBySong(song);
|
||||
|
||||
if (remoteLyricsResult) {
|
||||
remoteLyrics = {
|
||||
...remoteLyricsResult,
|
||||
lyrics: formatLyrics(remoteLyricsResult.lyrics),
|
||||
remote: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteLyrics) {
|
||||
return remoteLyrics;
|
||||
}
|
||||
|
||||
if (localLyrics) {
|
||||
return localLyrics;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
queryKey: queryKeys.songs.lyrics(args.serverId || '', args.query),
|
||||
staleTime: Infinity,
|
||||
...args.options,
|
||||
});
|
||||
},
|
||||
songLyricsByRemoteId: (args: QueryHookArgs<Partial<LyricGetQuery>>) => {
|
||||
return queryOptions({
|
||||
queryFn: async () => {
|
||||
const remoteLyricsResult = await lyricsIpc?.getRemoteLyricsByRemoteId(
|
||||
args.query as any,
|
||||
);
|
||||
|
||||
if (remoteLyricsResult) {
|
||||
return formatLyrics(remoteLyricsResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
queryKey: queryKeys.songs.lyricsByRemoteId(args.query),
|
||||
...args.options,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { openModal } from '@mantine/modals';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import styles from './lyrics-search-form.module.css';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { useLyricSearch } from '/@/renderer/features/lyrics/queries/lyric-search-query';
|
||||
import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||
@@ -75,9 +76,11 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch
|
||||
const [debouncedArtist] = useDebouncedValue(form.values.artist, 500);
|
||||
const [debouncedName] = useDebouncedValue(form.values.name, 500);
|
||||
|
||||
const { data, isInitialLoading } = useLyricSearch({
|
||||
query: { artist: debouncedArtist, name: debouncedName },
|
||||
});
|
||||
const { data, isInitialLoading } = useQuery(
|
||||
lyricsQueries.search({
|
||||
query: { artist: debouncedArtist, name: debouncedName },
|
||||
}),
|
||||
);
|
||||
|
||||
const searchResults = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
@@ -7,12 +8,9 @@ import styles from './lyrics.module.css';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||
import { translateLyrics } from '/@/renderer/features/lyrics/api/lyric-translate';
|
||||
import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api';
|
||||
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
||||
import {
|
||||
useSongLyricsByRemoteId,
|
||||
useSongLyricsBySong,
|
||||
} from '/@/renderer/features/lyrics/queries/lyric-query';
|
||||
import { translateLyrics } from '/@/renderer/features/lyrics/queries/lyric-translate';
|
||||
import {
|
||||
SynchronizedLyrics,
|
||||
SynchronizedLyricsProps,
|
||||
@@ -43,12 +41,14 @@ export const Lyrics = () => {
|
||||
const [translatedLyrics, setTranslatedLyrics] = useState<null | string>(null);
|
||||
const [showTranslation, setShowTranslation] = useState(false);
|
||||
|
||||
const { data, isInitialLoading } = useSongLyricsBySong(
|
||||
{
|
||||
query: { songId: currentSong?.id || '' },
|
||||
serverId: currentSong?.serverId || '',
|
||||
},
|
||||
currentSong,
|
||||
const { data, isInitialLoading } = useQuery(
|
||||
lyricsQueries.songLyrics(
|
||||
{
|
||||
query: { songId: currentSong?.id || '' },
|
||||
serverId: currentSong?.serverId || '',
|
||||
},
|
||||
currentSong,
|
||||
),
|
||||
);
|
||||
|
||||
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
|
||||
@@ -116,17 +116,19 @@ export const Lyrics = () => {
|
||||
await fetchTranslation();
|
||||
}, [translatedLyrics, showTranslation, fetchTranslation]);
|
||||
|
||||
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
|
||||
options: {
|
||||
enabled: !!override,
|
||||
},
|
||||
query: {
|
||||
remoteSongId: override?.id,
|
||||
remoteSource: override?.source as LyricSource | undefined,
|
||||
song: currentSong,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
const { isInitialLoading: isOverrideLoading } = useQuery(
|
||||
lyricsQueries.songLyricsByRemoteId({
|
||||
options: {
|
||||
enabled: !!override,
|
||||
},
|
||||
query: {
|
||||
remoteSongId: override?.id,
|
||||
remoteSource: override?.source as LyricSource | undefined,
|
||||
song: currentSong,
|
||||
},
|
||||
serverId: currentSong?.serverId || '',
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubSongChange = usePlayerStore.subscribe(
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import { useQuery, useQueryClient, UseQueryResult } from '@tanstack/react-query';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById, useLyricsSettings } from '/@/renderer/store';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import {
|
||||
FullLyricsMetadata,
|
||||
InternetProviderLyricResponse,
|
||||
LyricGetQuery,
|
||||
LyricsQuery,
|
||||
QueueSong,
|
||||
ServerType,
|
||||
StructuredLyric,
|
||||
SynchronizedLyricsArray,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
const lyricsIpc = isElectron() ? window.api.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<LyricsQuery>,
|
||||
): UseQueryResult<null | string> => {
|
||||
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<LyricsQuery>,
|
||||
song: QueueSong | undefined,
|
||||
): UseQueryResult<FullLyricsMetadata | StructuredLyric[]> => {
|
||||
const { query } = args;
|
||||
const { fetch, preferLocalLyrics } = useLyricsSettings();
|
||||
const server = getServerById(song?.serverId);
|
||||
|
||||
return useQuery({
|
||||
cacheTime: Infinity,
|
||||
enabled: !!song && !!server,
|
||||
onError: () => {},
|
||||
queryFn: async ({ signal }): Promise<FullLyricsMetadata | null | StructuredLyric[]> => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
if (!song) return null;
|
||||
|
||||
let localLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
|
||||
let remoteLyrics: FullLyricsMetadata | null | StructuredLyric[] = null;
|
||||
|
||||
if (hasFeature(server, ServerFeature.LYRICS_MULTIPLE_STRUCTURED)) {
|
||||
const subsonicLyrics = await api.controller
|
||||
.getStructuredLyrics({
|
||||
apiClientProps: { server, signal },
|
||||
query: { songId: song.id },
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
if (subsonicLyrics?.length) {
|
||||
localLyrics = subsonicLyrics;
|
||||
}
|
||||
} else if (hasFeature(server, ServerFeature.LYRICS_SINGLE_STRUCTURED)) {
|
||||
const jfLyrics = await api.controller
|
||||
.getLyrics({
|
||||
apiClientProps: { server, signal },
|
||||
query: { songId: song.id },
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
if (jfLyrics) {
|
||||
localLyrics = {
|
||||
artist: song.artists?.[0]?.name,
|
||||
lyrics: jfLyrics,
|
||||
name: song.name,
|
||||
remote: false,
|
||||
source: server?.name ?? 'music server',
|
||||
};
|
||||
}
|
||||
} else if (song.lyrics) {
|
||||
localLyrics = {
|
||||
artist: song.artists?.[0]?.name,
|
||||
lyrics: formatLyrics(song.lyrics),
|
||||
name: song.name,
|
||||
remote: false,
|
||||
source: server?.name ?? 'music server',
|
||||
};
|
||||
}
|
||||
|
||||
if (preferLocalLyrics && localLyrics) {
|
||||
return localLyrics;
|
||||
}
|
||||
|
||||
if (fetch) {
|
||||
const remoteLyricsResult: InternetProviderLyricResponse | null =
|
||||
await lyricsIpc?.getRemoteLyricsBySong(song);
|
||||
|
||||
if (remoteLyricsResult) {
|
||||
remoteLyrics = {
|
||||
...remoteLyricsResult,
|
||||
lyrics: formatLyrics(remoteLyricsResult.lyrics),
|
||||
remote: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteLyrics) {
|
||||
return remoteLyrics;
|
||||
}
|
||||
|
||||
if (localLyrics) {
|
||||
return localLyrics;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
queryKey: queryKeys.songs.lyrics(server?.id || '', query),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSongLyricsByRemoteId = (
|
||||
args: QueryHookArgs<Partial<LyricGetQuery>>,
|
||||
): UseQueryResult<null | string> => {
|
||||
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 any);
|
||||
|
||||
if (remoteLyricsResult) {
|
||||
return formatLyrics(remoteLyricsResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
queryKey: queryKeys.songs.lyricsByRemoteId(query),
|
||||
});
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||
import {
|
||||
InternetProviderLyricSearchResponse,
|
||||
LyricSearchQuery,
|
||||
LyricSource,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
const lyricsIpc = isElectron() ? window.api.lyrics : null;
|
||||
|
||||
export const useLyricSearch = (args: Omit<QueryHookArgs<LyricSearchQuery>, 'serverId'>) => {
|
||||
const { options, query } = args;
|
||||
|
||||
return useQuery<Record<LyricSource, InternetProviderLyricSearchResponse[]>>({
|
||||
cacheTime: 1000 * 60 * 1,
|
||||
enabled: !!query.artist || !!query.name,
|
||||
queryFn: () => {
|
||||
if (lyricsIpc) {
|
||||
return lyricsIpc.searchRemoteLyrics(query);
|
||||
}
|
||||
return {} as Record<LyricSource, InternetProviderLyricSearchResponse[]>;
|
||||
},
|
||||
queryKey: queryKeys.songs.lyricsSearch(query),
|
||||
staleTime: 1000 * 60 * 1,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user