mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-23 19:06:32 +02:00
Add support for OpenSubsonic enhanced lyrics (#2208)
This commit is contained in:
@@ -13,12 +13,20 @@ import {
|
||||
lyricsQueries,
|
||||
type LyricsQueryResult,
|
||||
} from '/@/renderer/features/lyrics/api/lyrics-api';
|
||||
import {
|
||||
formatStructuredLyricLabel,
|
||||
getLyricLineText,
|
||||
getLyricsLayers,
|
||||
lyricsHasWordCues,
|
||||
} from '/@/renderer/features/lyrics/api/lyrics-utils';
|
||||
import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form';
|
||||
import {
|
||||
useFuriganaLyrics,
|
||||
useRomajiLyrics,
|
||||
useSyncedRomajiLyrics,
|
||||
} from '/@/renderer/features/lyrics/hooks/use-furigana-lyrics';
|
||||
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
||||
import { SynchronizedKaraokeLyrics } from '/@/renderer/features/lyrics/synchronized-karaoke-lyrics';
|
||||
import {
|
||||
SynchronizedLyrics,
|
||||
SynchronizedLyricsProps,
|
||||
@@ -64,6 +72,8 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
const [index, setIndexState] = useState(0);
|
||||
const [translatedLyrics, setTranslatedLyrics] = useState<null | string>(null);
|
||||
const [showTranslation, setShowTranslation] = useState(false);
|
||||
const [showTranslationLayer, setShowTranslationLayer] = useState(false);
|
||||
const [showPronunciationLayer, setShowPronunciationLayer] = useState(false);
|
||||
const [pendingSongId, setPendingSongId] = useState<string | undefined>(currentSong?.id);
|
||||
const lyricsFetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const previousSongIdRef = useRef<string | undefined>(currentSong?.id);
|
||||
@@ -98,7 +108,10 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
return queryKeys.songs.lyrics(currentSong._serverId, { songId: currentSong.id });
|
||||
}, [currentSong]);
|
||||
|
||||
const { data, isLoading } = useQuery(
|
||||
const shouldFetchLyrics = !isLyricsDisabled && !!currentSong?._serverId && !!currentSong?.id;
|
||||
const isWaitingToFetchLyrics = shouldFetchLyrics && pendingSongId !== currentSong?.id;
|
||||
|
||||
const { data, isLoading, isRefetching } = useQuery(
|
||||
lyricsQueries.songLyrics(
|
||||
{
|
||||
options: {
|
||||
@@ -125,6 +138,14 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
const { data: furiganaConvertedLyrics } = useFuriganaLyrics(lyrics?.lyrics, !!enableFurigana);
|
||||
const { data: romajiConvertedLyrics } = useRomajiLyrics(lyrics?.lyrics, !!enableRomaji);
|
||||
|
||||
const rawSyncedLyrics = useMemo(() => {
|
||||
if (!synced || !lyrics || !('lyrics' in lyrics) || !Array.isArray(lyrics.lyrics)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return lyrics.lyrics;
|
||||
}, [lyrics, synced]);
|
||||
|
||||
const displayLyrics = useMemo(() => {
|
||||
if (isLyricsDisabled || !lyrics) return null;
|
||||
if (enableFurigana && furiganaConvertedLyrics) {
|
||||
@@ -138,7 +159,90 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
return getDisplayOffset(lyrics, data.selectedOffsetMs, indexToUse, data.local);
|
||||
}, [data, indexToUse, lyrics]);
|
||||
|
||||
const layers = useMemo(() => {
|
||||
if (!Array.isArray(data?.local)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getLyricsLayers(data.local);
|
||||
}, [data]);
|
||||
|
||||
const translationLyricsOverlay = useMemo(() => {
|
||||
if (!showTranslationLayer || !layers?.translation?.synced) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return layers.translation.lyrics;
|
||||
}, [layers, showTranslationLayer]);
|
||||
|
||||
const pronunciationLyricsOverlay = useMemo(() => {
|
||||
if (!showPronunciationLayer || !layers?.pronunciation?.synced) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return layers.pronunciation.lyrics;
|
||||
}, [layers, showPronunciationLayer]);
|
||||
|
||||
const selectedAgents = useMemo(() => {
|
||||
if (!lyrics || !('synced' in lyrics) || !lyrics.synced) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return lyrics.agents;
|
||||
}, [lyrics]);
|
||||
|
||||
const displayOffsetMs = isLyricsDisabled ? 0 : currentOffsetMs;
|
||||
const useServerPronunciation = !!pronunciationLyricsOverlay;
|
||||
|
||||
const { data: syncedRomajiLyrics } = useSyncedRomajiLyrics(
|
||||
rawSyncedLyrics,
|
||||
!!enableRomaji &&
|
||||
!!rawSyncedLyrics &&
|
||||
lyricsHasWordCues(rawSyncedLyrics) &&
|
||||
!useServerPronunciation,
|
||||
);
|
||||
|
||||
const isKaraoke = useMemo(() => {
|
||||
if (!synced || !displayLyrics || !('lyrics' in displayLyrics)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Array.isArray(displayLyrics.lyrics) && lyricsHasWordCues(displayLyrics.lyrics);
|
||||
}, [displayLyrics, synced]);
|
||||
|
||||
const syncedLyricsProps = useMemo(() => {
|
||||
if (!displayLyrics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...(displayLyrics as SynchronizedLyricsProps),
|
||||
offsetMs: displayOffsetMs,
|
||||
pronunciationLyrics: pronunciationLyricsOverlay,
|
||||
romajiLyrics:
|
||||
enableRomaji && !useServerPronunciation
|
||||
? (romajiConvertedLyrics as SynchronizedLyricsProps['romajiLyrics'])
|
||||
: null,
|
||||
settingsKey,
|
||||
syncedRomajiLyrics:
|
||||
enableRomaji && !useServerPronunciation ? (syncedRomajiLyrics ?? null) : null,
|
||||
translatedLyrics: showTranslation && !showTranslationLayer ? translatedLyrics : null,
|
||||
translationLyrics: translationLyricsOverlay,
|
||||
};
|
||||
}, [
|
||||
displayLyrics,
|
||||
displayOffsetMs,
|
||||
enableRomaji,
|
||||
pronunciationLyricsOverlay,
|
||||
romajiConvertedLyrics,
|
||||
syncedRomajiLyrics,
|
||||
settingsKey,
|
||||
showTranslation,
|
||||
showTranslationLayer,
|
||||
translatedLyrics,
|
||||
translationLyricsOverlay,
|
||||
useServerPronunciation,
|
||||
]);
|
||||
|
||||
const handleOnSearchOverride = useCallback(
|
||||
(params: LyricsOverride) => {
|
||||
@@ -219,7 +323,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
const fetchTranslation = useCallback(async () => {
|
||||
if (!lyrics || isLyricsDisabled) return;
|
||||
const originalLyrics = Array.isArray(lyrics.lyrics)
|
||||
? lyrics.lyrics.map(([, line]) => line).join('\n')
|
||||
? lyrics.lyrics.map((line) => getLyricLineText(line)).join('\n')
|
||||
: lyrics.lyrics;
|
||||
const TranslatedText: null | string = await translateLyrics(
|
||||
originalLyrics,
|
||||
@@ -250,6 +354,8 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
onCurrentSongChange: () => {
|
||||
setIndexState(0);
|
||||
setShowTranslation(false);
|
||||
setShowTranslationLayer(false);
|
||||
setShowPronunciationLayer(false);
|
||||
setTranslatedLyrics(null);
|
||||
},
|
||||
},
|
||||
@@ -265,7 +371,10 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
const languages = useMemo(() => {
|
||||
const local = data?.local;
|
||||
if (Array.isArray(local)) {
|
||||
return local.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
|
||||
return local.map((lyric, idx) => ({
|
||||
label: formatStructuredLyricLabel(lyric),
|
||||
value: idx.toString(),
|
||||
}));
|
||||
}
|
||||
if (local && !Array.isArray(local) && 'lyrics' in local) {
|
||||
return [{ label: 'xxx', value: '0' }];
|
||||
@@ -273,7 +382,8 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
return [];
|
||||
}, [data?.local]);
|
||||
|
||||
const isLoadingLyrics = isLoading && !isLyricsDisabled;
|
||||
const isLoadingLyrics =
|
||||
shouldFetchLyrics && (isWaitingToFetchLyrics || isLoading || isRefetching);
|
||||
const hasNoLyrics = !displayLyrics;
|
||||
const [shouldFadeOut, setShouldFadeOut] = useState(false);
|
||||
|
||||
@@ -325,7 +435,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
) : (
|
||||
<AnimatePresence mode="sync">
|
||||
{hasNoLyrics ? (
|
||||
<Center w="100%">
|
||||
<Center flex={1} w="100%">
|
||||
<motion.div
|
||||
animate={{ opacity: shouldFadeOut ? 0 : 1 }}
|
||||
initial={{ opacity: 1 }}
|
||||
@@ -345,18 +455,15 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
initial={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{synced ? (
|
||||
<SynchronizedLyrics
|
||||
{...(displayLyrics as SynchronizedLyricsProps)}
|
||||
offsetMs={displayOffsetMs}
|
||||
romajiLyrics={
|
||||
enableRomaji
|
||||
? (romajiConvertedLyrics as SynchronizedLyricsProps['romajiLyrics'])
|
||||
: null
|
||||
}
|
||||
settingsKey={settingsKey}
|
||||
translatedLyrics={showTranslation ? translatedLyrics : null}
|
||||
/>
|
||||
{synced && syncedLyricsProps ? (
|
||||
isKaraoke ? (
|
||||
<SynchronizedKaraokeLyrics
|
||||
{...syncedLyricsProps}
|
||||
agents={selectedAgents}
|
||||
/>
|
||||
) : (
|
||||
<SynchronizedLyrics {...syncedLyricsProps} />
|
||||
)
|
||||
) : (
|
||||
<UnsynchronizedLyrics
|
||||
{...(displayLyrics as UnsynchronizedLyricsProps)}
|
||||
@@ -376,12 +483,20 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
<div className={styles.actionsContainer}>
|
||||
<LyricsActions
|
||||
hasLyrics={!!displayLyrics}
|
||||
hasPronunciationLayer={!!layers?.pronunciation?.synced}
|
||||
hasTranslationLayer={!!layers?.translation?.synced}
|
||||
index={indexToUse}
|
||||
languages={languages}
|
||||
offsetMs={displayOffsetMs}
|
||||
onExportLyrics={handleExportLyrics}
|
||||
onRemoveLyric={handleOnRemoveLyric}
|
||||
onSearchOverride={handleOnSearchOverride}
|
||||
onTogglePronunciationLayer={() =>
|
||||
setShowPronunciationLayer((current) => !current)
|
||||
}
|
||||
onToggleTranslationLayer={() =>
|
||||
setShowTranslationLayer((current) => !current)
|
||||
}
|
||||
onTranslateLyric={
|
||||
translationApiProvider && translationApiKey
|
||||
? handleOnTranslateLyric
|
||||
@@ -390,6 +505,8 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
||||
onUpdateOffset={handleUpdateOffset}
|
||||
setIndex={setIndex}
|
||||
settingsKey={settingsKey}
|
||||
showPronunciationLayer={showPronunciationLayer}
|
||||
showTranslationLayer={showTranslationLayer}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user