mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-24 03:16:43 +02:00
implement OS enhanced/karaoke lyrics
This commit is contained in:
@@ -1,10 +1,78 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
import { LyricsResponse, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
|
||||
import {
|
||||
alignFuriganaToWordCues,
|
||||
alignRomajiTokensToWordCues,
|
||||
LyricTextToken,
|
||||
RomajiToken,
|
||||
} from '/@/renderer/features/lyrics/api/lyric-conversion';
|
||||
import { LyricsResponse, SyncedCueLine, SynchronizedLyrics } from '/@/shared/types/domain-types';
|
||||
|
||||
const lyricsApi = isElectron() ? window.api.lyrics : null;
|
||||
|
||||
const convertSyncedLyricsFurigana = async (
|
||||
lyrics: SynchronizedLyrics,
|
||||
): Promise<SynchronizedLyrics> => {
|
||||
if (!lyricsApi) {
|
||||
return lyrics;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
lyrics.map(async (line) => ({
|
||||
...line,
|
||||
cueLines: line.cueLines
|
||||
? await Promise.all(
|
||||
line.cueLines.map(async (cueLine) => {
|
||||
const tokens = (await lyricsApi.parseLyricsTextTokens(
|
||||
cueLine.value,
|
||||
)) as LyricTextToken[];
|
||||
const alignedWords = cueLine.words.length
|
||||
? await alignFuriganaToWordCues(
|
||||
cueLine.value,
|
||||
cueLine.words,
|
||||
tokens,
|
||||
(text) => lyricsApi.convertFuriganaFragment(text),
|
||||
)
|
||||
: cueLine.words;
|
||||
return {
|
||||
...cueLine,
|
||||
value: await lyricsApi.convertFurigana(cueLine.value),
|
||||
words: alignedWords ?? cueLine.words,
|
||||
};
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
text: await lyricsApi.convertFurigana(line.text),
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const convertSyncedLyricsRomaji = async (
|
||||
lyrics: SynchronizedLyrics,
|
||||
convert: (text: string) => Promise<string>,
|
||||
): Promise<SynchronizedLyrics> =>
|
||||
Promise.all(
|
||||
lyrics.map(async (line) => ({
|
||||
...line,
|
||||
cueLines: line.cueLines
|
||||
? await Promise.all(
|
||||
line.cueLines.map(async (cueLine) => ({
|
||||
...cueLine,
|
||||
value: await convert(cueLine.value),
|
||||
words: await Promise.all(
|
||||
cueLine.words.map(async (word) => ({
|
||||
...word,
|
||||
text: await convert(word.text),
|
||||
})),
|
||||
),
|
||||
})),
|
||||
)
|
||||
: undefined,
|
||||
text: await convert(line.text),
|
||||
})),
|
||||
);
|
||||
|
||||
export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
|
||||
return useQuery({
|
||||
enabled: enabled && !!lyrics && !!lyricsApi,
|
||||
@@ -13,15 +81,12 @@ export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, ena
|
||||
|
||||
if (typeof lyrics === 'string') {
|
||||
return await lyricsApi.convertFurigana(lyrics);
|
||||
} else if (Array.isArray(lyrics)) {
|
||||
const converted = await Promise.all(
|
||||
lyrics.map(async ([time, line]) => [
|
||||
time,
|
||||
await lyricsApi.convertFurigana(line),
|
||||
]),
|
||||
);
|
||||
return converted as SynchronizedLyricsArray;
|
||||
}
|
||||
|
||||
if (Array.isArray(lyrics)) {
|
||||
return convertSyncedLyricsFurigana(lyrics);
|
||||
}
|
||||
|
||||
return lyrics;
|
||||
},
|
||||
queryKey: ['furigana', lyrics],
|
||||
@@ -37,15 +102,80 @@ export const useRomajiLyrics = (lyrics: LyricsResponse | null | undefined, enabl
|
||||
|
||||
if (typeof lyrics === 'string') {
|
||||
return await lyricsApi.convertRomaji(lyrics);
|
||||
} else if (Array.isArray(lyrics)) {
|
||||
const converted = await Promise.all(
|
||||
lyrics.map(async ([time, line]) => [time, await lyricsApi.convertRomaji(line)]),
|
||||
);
|
||||
return converted as SynchronizedLyricsArray;
|
||||
}
|
||||
|
||||
if (Array.isArray(lyrics)) {
|
||||
return convertSyncedLyricsRomaji(lyrics, (text) => lyricsApi.convertRomaji(text));
|
||||
}
|
||||
|
||||
return lyrics;
|
||||
},
|
||||
queryKey: ['romaji', lyrics],
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
|
||||
export type SyncedRomajiLyrics = (null | SyncedCueLine[])[];
|
||||
|
||||
const buildSyncedRomajiLine = async (
|
||||
cueLines: SyncedCueLine[],
|
||||
): Promise<null | SyncedCueLine[]> => {
|
||||
const romajiCueLines: SyncedCueLine[] = [];
|
||||
|
||||
for (const cueLine of cueLines) {
|
||||
if (!cueLine.words.length) {
|
||||
romajiCueLines.push({ ...cueLine });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lyricsApi) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokens = (await lyricsApi.convertRomajiTokens(cueLine.value)) as RomajiToken[];
|
||||
const alignedWords = alignRomajiTokensToWordCues(cueLine.value, cueLine.words, tokens);
|
||||
|
||||
if (!alignedWords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
romajiCueLines.push({
|
||||
...cueLine,
|
||||
words: alignedWords,
|
||||
});
|
||||
}
|
||||
|
||||
return romajiCueLines;
|
||||
};
|
||||
|
||||
export const useSyncedRomajiLyrics = (
|
||||
lyrics: null | SynchronizedLyrics | undefined,
|
||||
enabled: boolean,
|
||||
) => {
|
||||
return useQuery({
|
||||
enabled: enabled && !!lyrics && !!lyricsApi,
|
||||
queryFn: async (): Promise<null | SyncedRomajiLyrics> => {
|
||||
if (!lyrics || !lyricsApi || !enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: SyncedRomajiLyrics = [];
|
||||
|
||||
for (const line of lyrics) {
|
||||
if (
|
||||
!line.cueLines?.length ||
|
||||
!line.cueLines.some((cueLine) => cueLine.words.length)
|
||||
) {
|
||||
result.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(await buildSyncedRomajiLine(line.cueLines));
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
queryKey: ['romaji-synced', lyrics],
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user