feat: 逐字歌词

This commit is contained in:
alger
2025-10-11 20:23:54 +08:00
parent 4575e4f26d
commit cb2baeadf5
10 changed files with 877 additions and 367 deletions

View File

@@ -65,7 +65,7 @@ export const getMusicLrc = async (id: number) => {
} }
// 获取新的歌词数据 // 获取新的歌词数据
const res = await request.get<ILyric>('/lyric', { params: { id } }); const res = await request.get<ILyric>('/lyric/new', { params: { id } });
// 只有在成功获取新数据后才删除旧缓存并添加新缓存 // 只有在成功获取新数据后才删除旧缓存并添加新缓存
if (res?.data) { if (res?.data) {

View File

@@ -90,10 +90,11 @@ export class CacheManager {
musicSources?: string[] musicSources?: string[]
): Promise<void> { ): Promise<void> {
try { try {
// 深度克隆数据,确保可以被 IndexedDB 存储
await saveData('music_url_cache', { await saveData('music_url_cache', {
id, id,
data: result, data: cloneDeep(result),
musicSources: musicSources || [], musicSources: cloneDeep(musicSources || []),
createTime: Date.now() createTime: Date.now()
}); });
console.log(`缓存音乐URL成功: ${id}`); console.log(`缓存音乐URL成功: ${id}`);

View File

@@ -119,7 +119,22 @@
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }" :class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
@click="setAudioTime(index)" @click="setAudioTime(index)"
> >
<span :style="getLrcStyle(index)">{{ item.text }}</span> <!-- 逐字歌词显示 -->
<div
v-if="item.hasWordByWord && item.words && item.words.length > 0"
class="word-by-word-lyric"
>
<span
v-for="(word, wordIndex) in item.words"
:key="wordIndex"
class="lyric-word"
:style="getWordStyle(index, wordIndex, word)"
>
{{ word.text }}
</span>
</div>
<!-- 普通歌词显示 -->
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
<div v-show="config.showTranslation" class="music-lrc-text-tr"> <div v-show="config.showTranslation" class="music-lrc-text-tr">
{{ item.trText }} {{ item.trText }}
</div> </div>
@@ -144,7 +159,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Cover3D from '@/components/cover/Cover3D.vue'; import Cover3D from '@/components/cover/Cover3D.vue';
@@ -157,6 +172,7 @@ import {
correctionTime, correctionTime,
lrcArray, lrcArray,
nowIndex, nowIndex,
nowTime,
playMusic, playMusic,
setAudioTime, setAudioTime,
textColors, textColors,
@@ -179,6 +195,7 @@ const animationFrame = ref<number | null>(null);
const isDark = ref(false); const isDark = ref(false);
const showStickyHeader = ref(false); const showStickyHeader = ref(false);
const lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>(); const lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>();
const isSongChanging = ref(false);
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG }); const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
@@ -274,6 +291,8 @@ const mouseLeaveLayout = () => {
}; };
watch(nowIndex, () => { watch(nowIndex, () => {
// 歌曲切换时不自动滚动
if (isSongChanging.value) return;
debouncedLrcScroll(); debouncedLrcScroll();
}); });
@@ -337,25 +356,30 @@ watch(
{ immediate: true } { immediate: true }
); );
// 修改 useLyricProgress 的使用方式
const { getLrcStyle: originalLrcStyle } = useLyricProgress(); const { getLrcStyle: originalLrcStyle } = useLyricProgress();
// 修改 getLrcStyle 函数
const getLrcStyle = (index: number) => { const getLrcStyle = (index: number) => {
const colors = textColors.value || getTextColors; const colors = textColors.value || getTextColors();
const originalStyle = originalLrcStyle(index); const originalStyle = originalLrcStyle(index);
if (index === nowIndex.value) { if (index === nowIndex.value) {
// 当前播放的歌词,使用渐变效果 // 当前播放的歌词
return { if (originalStyle.backgroundImage) {
...originalStyle, // 有渐变进度时,使用渐变效果
backgroundImage: originalStyle.backgroundImage return {
?.replace(/#ffffff/g, colors.active) ...originalStyle,
.replace(/#ffffff8a/g, `${colors.primary}`), backgroundImage: originalStyle.backgroundImage
backgroundClip: 'text', .replace(/#ffffff/g, colors.active)
WebkitBackgroundClip: 'text', .replace(/#ffffff8a/g, `${colors.primary}`),
color: 'transparent' backgroundClip: 'text',
}; WebkitBackgroundClip: 'text',
color: 'transparent'
};
} else {
return {
color: colors.primary
};
}
} }
// 非当前播放的歌词,使用普通颜色 // 非当前播放的歌词,使用普通颜色
@@ -364,6 +388,57 @@ const getLrcStyle = (index: number) => {
}; };
}; };
// 逐字歌词样式函数
const getWordStyle = (lineIndex: number, _wordIndex: number, word: any) => {
const colors = textColors.value || getTextColors();
// 如果不是当前行,返回普通样式
if (lineIndex !== nowIndex.value) {
return {
color: colors.primary,
transition: 'color 0.3s ease',
// 重置背景相关属性
backgroundImage: 'none',
WebkitTextFillColor: 'initial'
};
}
// 当前行的逐字效果,应用歌词矫正时间
const currentTime = (nowTime.value + correctionTime.value) * 1000; // 转换为毫秒确保与word时间单位一致
// 直接使用绝对时间比较
const wordStartTime = word.startTime; // 单词开始的绝对时间(毫秒)
const wordEndTime = word.startTime + word.duration;
if (currentTime >= wordStartTime && currentTime < wordEndTime) {
// 当前正在播放的单词 - 使用渐变进度效果
const progress = Math.min((currentTime - wordStartTime) / word.duration, 1);
const progressPercent = Math.round(progress * 100);
return {
backgroundImage: `linear-gradient(to right, ${colors.active} 0%, ${colors.active} ${progressPercent}%, ${colors.primary} ${progressPercent}%, ${colors.primary} 100%)`,
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
textShadow: `0 0 8px ${colors.active}40`,
transition: 'all 0.1s ease'
};
} else if (currentTime >= wordEndTime) {
// 已经播放过的单词 - 纯色显示
return {
color: colors.active,
WebkitTextFillColor: 'initial',
transition: 'none'
};
} else {
// 还未播放的单词 - 普通状态
return {
color: colors.primary,
WebkitTextFillColor: 'initial',
transition: 'none'
};
}
};
// 组件卸载时清理动画 // 组件卸载时清理动画
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (animationFrame.value) { if (animationFrame.value) {
@@ -502,12 +577,24 @@ onMounted(() => {
} }
}); });
// 添加对 playMusic 的监听 // 添加对 playMusic.id 的监听,歌曲切换时滚动到顶部
watch(playMusic, () => { watch(
nextTick(() => { () => playMusic.value.id,
lrcScroll('instant', true); (newId, oldId) => {
}); // 只在歌曲真正切换时滚动到顶部
}); if (newId !== oldId && newId) {
isSongChanging.value = true;
// 延迟滚动,确保 nowIndex 已重置
setTimeout(() => {
lrcScroll('instant', true);
// 延迟恢复自动滚动,等待歌词数据更新
setTimeout(() => {
isSongChanging.value = false;
}, 300);
}, 100);
}
}
);
defineExpose({ defineExpose({
lrcScroll, lrcScroll,
@@ -621,6 +708,9 @@ defineExpose({
.music-lrc-container { .music-lrc-container {
padding-top: 30vh; padding-top: 30vh;
.music-lrc-text:last-child {
margin-bottom: 200px;
}
} }
.music-lrc { .music-lrc {
@@ -670,6 +760,28 @@ defineExpose({
opacity: 0.7; opacity: 0.7;
color: var(--text-color-primary); color: var(--text-color-primary);
} }
// 逐字歌词样式
.word-by-word-lyric {
@apply flex flex-wrap;
.lyric-word {
@apply inline-block;
margin-right: 4px;
padding: 2px 4px;
border-radius: 4px;
font-weight: inherit;
font-size: inherit;
letter-spacing: inherit;
line-height: inherit;
cursor: inherit;
position: relative;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
}
} }
.hover-text { .hover-text {

View File

@@ -57,7 +57,22 @@
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }" :class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
@click="jumpToLyricTime(index)" @click="jumpToLyricTime(index)"
> >
<span :style="getLrcStyle(index)">{{ item.text }}</span> <!-- 逐字歌词显示 -->
<div
v-if="item.hasWordByWord && item.words && item.words.length > 0"
class="word-by-word-lyric"
>
<span
v-for="(word, wordIndex) in item.words"
:key="wordIndex"
class="lyric-word"
:style="getWordStyle(index, wordIndex, word)"
>
{{ word.text }}
</span>
</div>
<!-- 普通歌词显示 -->
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
<div v-if="config.showTranslation && item.trText" class="translation"> <div v-if="config.showTranslation && item.trText" class="translation">
{{ item.trText }} {{ item.trText }}
</div> </div>
@@ -119,7 +134,22 @@
<div class="lyrics-container" v-if="!config.hideLyrics" @click="showFullLyricScreen"> <div class="lyrics-container" v-if="!config.hideLyrics" @click="showFullLyricScreen">
<div v-if="lrcArray.length > 0" class="lyrics-wrapper"> <div v-if="lrcArray.length > 0" class="lyrics-wrapper">
<div v-for="(line, idx) in visibleLyrics" :key="idx" class="lyric-line"> <div v-for="(line, idx) in visibleLyrics" :key="idx" class="lyric-line">
{{ line.text }} <!-- 逐字歌词显示 -->
<div
v-if="line.hasWordByWord && line.words && line.words.length > 0"
class="word-by-word-lyric"
>
<span
v-for="(word, wordIndex) in line.words"
:key="wordIndex"
class="lyric-word"
:style="getWordStyle(line.originalIndex, wordIndex, word)"
>
{{ word.text }}
</span>
</div>
<!-- 普通歌词显示 -->
<span v-else>{{ line.text }}</span>
</div> </div>
</div> </div>
<div v-else class="no-lyrics"> <div v-else class="no-lyrics">
@@ -225,7 +255,22 @@
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }" :class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
@click="jumpToLyricTime(index)" @click="jumpToLyricTime(index)"
> >
<span :style="getLrcStyle(index)">{{ item.text }}</span> <!-- 逐字歌词显示 -->
<div
v-if="item.hasWordByWord && item.words && item.words.length > 0"
class="word-by-word-lyric"
>
<span
v-for="(word, wordIndex) in item.words"
:key="wordIndex"
class="lyric-word"
:style="getWordStyle(index, wordIndex, word)"
>
{{ word.text }}
</span>
</div>
<!-- 普通歌词显示 -->
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
<div v-if="config.showTranslation && item.trText" class="translation"> <div v-if="config.showTranslation && item.trText" class="translation">
{{ item.trText }} {{ item.trText }}
</div> </div>
@@ -770,7 +815,11 @@ const visibleLyrics = computed(() => {
startIdx = Math.max(0, endIdx - numLines + 1); startIdx = Math.max(0, endIdx - numLines + 1);
} }
return lrcArray.value.slice(startIdx, endIdx + 1); // 返回带有原始索引的歌词数组
return lrcArray.value.slice(startIdx, endIdx + 1).map((item, idx) => ({
...item,
originalIndex: startIdx + idx
}));
}); });
const props = defineProps({ const props = defineProps({
@@ -1020,6 +1069,57 @@ const getLrcStyle = (index: number) => {
color: colors.primary color: colors.primary
}; };
}; };
// 逐字歌词样式函数
const getWordStyle = (lineIndex: number, _wordIndex: number, word: any) => {
const colors = textColors.value || getTextColors();
// 如果不是当前行,返回普通样式
if (lineIndex !== nowIndex.value) {
return {
color: colors.primary,
transition: 'color 0.3s ease',
// 重置背景相关属性
backgroundImage: 'none',
WebkitTextFillColor: 'initial'
};
}
// 当前行的逐字效果
const currentTime = nowTime.value * 1000; // 转换为毫秒确保与word时间单位一致
// 直接使用绝对时间比较
const wordStartTime = word.startTime; // 单词开始的绝对时间(毫秒)
const wordEndTime = word.startTime + word.duration;
if (currentTime >= wordStartTime && currentTime < wordEndTime) {
// 当前正在播放的单词 - 使用渐变进度效果
const progress = Math.min((currentTime - wordStartTime) / word.duration, 1);
const progressPercent = Math.round(progress * 100);
return {
backgroundImage: `linear-gradient(to right, ${colors.active} 0%, ${colors.active} ${progressPercent}%, ${colors.primary} ${progressPercent}%, ${colors.primary} 100%)`,
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
textShadow: `0 0 8px ${colors.active}40`,
transition: 'all 0.1s ease'
};
} else if (currentTime >= wordEndTime) {
// 已经播放过的单词 - 纯色显示
return {
color: colors.active,
WebkitTextFillColor: 'initial',
transition: 'none'
};
} else {
// 还未播放的单词 - 普通状态
return {
color: colors.primary,
WebkitTextFillColor: 'initial',
transition: 'none'
};
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1574,7 +1674,7 @@ const getLrcStyle = (index: number) => {
// 通用歌词样式 // 通用歌词样式
.lyric-line { .lyric-line {
@apply cursor-pointer transition-all duration-300; @apply cursor-pointer transition-all duration-300 font-medium;
font-weight: 500; font-weight: 500;
letter-spacing: var(--lyric-letter-spacing, 0); letter-spacing: var(--lyric-letter-spacing, 0);
line-height: var(--lyric-line-height, 1.6); line-height: var(--lyric-line-height, 1.6);
@@ -1587,7 +1687,7 @@ const getLrcStyle = (index: number) => {
} }
&.now-text { &.now-text {
@apply font-bold py-4; @apply font-medium py-4;
color: var(--text-color-active); color: var(--text-color-active);
opacity: 1; opacity: 1;
} }
@@ -1599,6 +1699,25 @@ const getLrcStyle = (index: number) => {
.translation { .translation {
@apply font-normal opacity-70 mt-1 text-base; @apply font-normal opacity-70 mt-1 text-base;
} }
// 逐字歌词样式
.word-by-word-lyric {
@apply flex flex-wrap justify-center;
.lyric-word {
@apply inline-block;
font-weight: inherit;
font-size: inherit;
letter-spacing: inherit;
line-height: inherit;
cursor: inherit;
position: relative;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
}
} }
// 全屏歌词相关样式 // 全屏歌词相关样式
@@ -1765,6 +1884,10 @@ const getLrcStyle = (index: number) => {
} }
} }
.lyric-word {
@apply px-[2px];
}
.no-lyrics { .no-lyrics {
@apply text-center text-base opacity-60; @apply text-center text-base opacity-60;
color: var(--text-color-primary); color: var(--text-color-primary);

View File

@@ -202,7 +202,7 @@ watch(
<style lang="scss" scoped> <style lang="scss" scoped>
.mobile-play-bar { .mobile-play-bar {
@apply fixed bottom-[56px] left-0 w-full flex flex-col; @apply fixed bottom-[76px] left-0 w-full flex flex-col;
z-index: 10000; z-index: 10000;
animation-duration: 0.3s !important; animation-duration: 0.3s !important;
transition: all 0.3s ease; transition: all 0.3s ease;

View File

@@ -10,6 +10,7 @@ import { getSongUrl } from '@/store/modules/player';
import type { Artist, ILyricText, SongResult } from '@/types/music'; import type { Artist, ILyricText, SongResult } from '@/types/music';
import { isElectron } from '@/utils'; import { isElectron } from '@/utils';
import { getTextColors } from '@/utils/linearColor'; import { getTextColors } from '@/utils/linearColor';
import { parseLyrics } from '@/utils/yrcParser';
const windowData = window as any; const windowData = window as any;
@@ -64,7 +65,7 @@ export const musicDB = await useIndexedDB(
{ name: 'music_url_cache', keyPath: 'id' }, { name: 'music_url_cache', keyPath: 'id' },
{ name: 'music_failed_cache', keyPath: 'id' } { name: 'music_failed_cache', keyPath: 'id' }
], ],
2 3
); );
// 键盘事件处理器,在初始化后设置 // 键盘事件处理器,在初始化后设置
@@ -268,27 +269,119 @@ const initProgressAnimation = () => {
}); });
}; };
/**
* 解析歌词字符串并转换为ILyricText格式
* @param lyricsStr 歌词字符串
* @returns 解析后的歌词数据
*/
const parseLyricsString = async (
lyricsStr: string
): Promise<{ lrcArray: ILyricText[]; lrcTimeArray: number[]; hasWordByWord: boolean }> => {
if (!lyricsStr || typeof lyricsStr !== 'string') {
return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };
}
try {
const parseResult = parseLyrics(lyricsStr);
if (!parseResult.success) {
console.error('歌词解析失败:', parseResult.error.message);
return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };
}
const { lyrics } = parseResult.data;
const lrcArray: ILyricText[] = [];
const lrcTimeArray: number[] = [];
let hasWordByWord = false;
for (const line of lyrics) {
// 检查是否有逐字歌词
const hasWords = line.words && line.words.length > 0;
if (hasWords) {
hasWordByWord = true;
}
lrcArray.push({
text: line.fullText,
trText: '', // 翻译文本稍后处理
words: hasWords
? line.words.map((word) => ({
text: word.text,
startTime: word.startTime,
duration: word.duration
}))
: undefined,
hasWordByWord: hasWords,
startTime: line.startTime,
duration: line.duration
});
lrcTimeArray.push(line.startTime);
}
console.log('parseLyricsString', lrcArray);
return { lrcArray, lrcTimeArray, hasWordByWord };
} catch (error) {
console.error('解析歌词时发生错误:', error);
return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };
}
};
// 设置音乐相关的监听器 // 设置音乐相关的监听器
const setupMusicWatchers = () => { const setupMusicWatchers = () => {
const store = getPlayerStore(); const store = getPlayerStore();
// 监听 playerStore.playMusic 的变化以更新歌词数据 // 监听 playerStore.playMusic 的变化以更新歌词数据
watch( watch(
() => store.playMusic, () => store.playMusic.id,
() => { async (newId, oldId) => {
nextTick(async () => { // 如果没有歌曲ID清空歌词
console.log('歌曲切换,更新歌词数据'); if (!newId) {
// 更新歌词数据 lrcArray.value = [];
const rawLrc = playMusic.value.lyric?.lrcArray || []; lrcTimeArray.value = [];
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || []; nowIndex.value = 0;
try { return;
const { translateLyrics } = await import('@/services/lyricTranslation'); }
lrcArray.value = await translateLyrics(rawLrc as any);
} catch (e) {
console.error('翻译歌词失败,使用原始歌词:', e);
lrcArray.value = rawLrc as any;
}
// 避免相同ID的重复执行(但允许初始化时执行)
if (newId === oldId && lrcArray.value.length > 0) return;
// 歌曲切换时重置歌词索引
if (newId !== oldId) {
nowIndex.value = 0;
}
await nextTick(async () => {
console.log('歌曲切换,更新歌词数据');
// 检查是否有原始歌词字符串需要解析
const lyricData = playMusic.value.lyric;
if (lyricData && typeof lyricData === 'string') {
// 如果歌词是字符串格式,使用新的解析器
const {
lrcArray: parsedLrcArray,
lrcTimeArray: parsedTimeArray,
hasWordByWord
} = await parseLyricsString(lyricData);
lrcArray.value = parsedLrcArray;
lrcTimeArray.value = parsedTimeArray;
// 更新歌曲的歌词数据结构
if (playMusic.value.lyric && typeof playMusic.value.lyric === 'object') {
playMusic.value.lyric.hasWordByWord = hasWordByWord;
}
} else {
// 使用现有的歌词数据结构
const rawLrc = lyricData?.lrcArray || [];
lrcTimeArray.value = lyricData?.lrcTimeArray || [];
try {
const { translateLyrics } = await import('@/services/lyricTranslation');
lrcArray.value = await translateLyrics(rawLrc as any);
} catch (e) {
console.error('翻译歌词失败,使用原始歌词:', e);
lrcArray.value = rawLrc as any;
}
}
// 当歌词数据更新时,如果歌词窗口打开,则发送数据 // 当歌词数据更新时,如果歌词窗口打开,则发送数据
if (isElectron && isLyricWindowOpen.value) { if (isElectron && isLyricWindowOpen.value) {
console.log('歌词窗口已打开,同步最新歌词数据'); console.log('歌词窗口已打开,同步最新歌词数据');
@@ -302,10 +395,7 @@ const setupMusicWatchers = () => {
} }
}); });
}, },
{ { immediate: true }
deep: true,
immediate: true
}
); );
}; };
@@ -563,20 +653,46 @@ export const adjustCorrectionTime = (delta: number) => {
// 获取当前播放歌词 // 获取当前播放歌词
export const isCurrentLrc = (index: number, time: number): boolean => { export const isCurrentLrc = (index: number, time: number): boolean => {
const currentTime = lrcTimeArray.value[index]; const currentTime = lrcTimeArray.value[index];
// 如果是最后一句歌词,只需要判断时间是否大于等于当前句的开始时间
if (index === lrcTimeArray.value.length - 1) {
const correctedTime = time + correctionTime.value;
return correctedTime >= currentTime;
}
// 非最后一句歌词,需要判断时间在当前句和下一句之间
const nextTime = lrcTimeArray.value[index + 1]; const nextTime = lrcTimeArray.value[index + 1];
const correctedTime = time + correctionTime.value; const correctedTime = time + correctionTime.value;
return correctedTime > currentTime && correctedTime < nextTime; return correctedTime >= currentTime && correctedTime < nextTime;
}; };
// 获取当前播放歌词INDEX // 获取当前播放歌词INDEX
export const getLrcIndex = (time: number): number => { export const getLrcIndex = (time: number): number => {
const correctedTime = time + correctionTime.value; const correctedTime = time + correctionTime.value;
for (let i = 0; i < lrcTimeArray.value.length; i++) {
if (isCurrentLrc(i, correctedTime - correctionTime.value)) { // 如果歌词数组为空,返回当前索引
if (lrcTimeArray.value.length === 0) {
return nowIndex.value;
}
// 处理最后一句歌词的情况
const lastIndex = lrcTimeArray.value.length - 1;
if (correctedTime >= lrcTimeArray.value[lastIndex]) {
nowIndex.value = lastIndex;
return lastIndex;
}
// 查找当前时间对应的歌词索引
for (let i = 0; i < lrcTimeArray.value.length - 1; i++) {
const currentTime = lrcTimeArray.value[i];
const nextTime = lrcTimeArray.value[i + 1];
if (correctedTime >= currentTime && correctedTime < nextTime) {
nowIndex.value = i; nowIndex.value = i;
return i; return i;
} }
} }
return nowIndex.value; return nowIndex.value;
}; };
@@ -833,6 +949,9 @@ onUnmounted(() => {
stopLyricSync(); stopLyricSync();
}); });
// 导出歌词解析函数供外部使用
export { parseLyricsString };
// 添加播放控制命令监听 // 添加播放控制命令监听
if (isElectron) { if (isElectron) {
windowData.electron.ipcRenderer.on('lyric-control-back', (_, command: string) => { windowData.electron.ipcRenderer.on('lyric-control-back', (_, command: string) => {

View File

@@ -1,286 +0,0 @@
import { Howl } from 'howler';
import { cloneDeep } from 'lodash';
import { ref } from 'vue';
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { audioService } from '@/services/audioService';
import { useSettingsStore } from '@/store';
import type { ILyric, ILyricText, SongResult } from '@/types/music';
import { getImgUrl } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor';
const musicHistory = useMusicHistory();
// 获取歌曲url
export const getSongUrl = async (id: any, songData: any, isDownloaded: boolean = false) => {
const settingsStore = useSettingsStore();
const { unlimitedDownload } = settingsStore.setData;
const { data } = await getMusicUrl(id, !unlimitedDownload);
let url = '';
let songDetail: any = null;
try {
if (data.data[0].freeTrialInfo || !data.data[0].url) {
const res = await getParsingMusicUrl(id, cloneDeep(songData));
if (res.data.data?.url) {
url = res.data.data.url;
songDetail = res.data.data;
}
} else {
songDetail = data.data[0] as any;
}
} catch (error) {
console.error('error', error);
}
if (isDownloaded) {
return songDetail;
}
url = url || data.data[0].url;
return url;
};
const getSongDetail = async (playMusic: SongResult) => {
playMusic.playLoading = true;
const playMusicUrl =
playMusic.playMusicUrl || (await getSongUrl(playMusic.id, cloneDeep(playMusic)));
const { backgroundColor, primaryColor } =
playMusic.backgroundColor && playMusic.primaryColor
? playMusic
: await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));
playMusic.playLoading = false;
return { ...playMusic, playMusicUrl, backgroundColor, primaryColor };
};
// 加载 当前歌曲 歌曲列表数据 下一首mp3预加载 歌词数据
export const useMusicListHook = () => {
const handlePlayMusic = async (state: any, playMusic: SongResult, isPlay: boolean = true) => {
const updatedPlayMusic = await getSongDetail(playMusic);
state.playMusic = updatedPlayMusic;
state.playMusicUrl = updatedPlayMusic.playMusicUrl;
// 记录当前设置的播放状态
state.play = isPlay;
// 每次设置新歌曲时,立即更新 localStorage
localStorage.setItem('currentPlayMusic', JSON.stringify(state.playMusic));
localStorage.setItem('currentPlayMusicUrl', state.playMusicUrl);
localStorage.setItem('isPlaying', state.play.toString());
// 设置网页标题
document.title = `${updatedPlayMusic.name} - ${updatedPlayMusic?.song?.artists?.reduce((prev, curr) => `${prev}${curr.name}/`, '')}`;
loadLrcAsync(state, updatedPlayMusic.id);
musicHistory.addMusic(state.playMusic);
const playListIndex = state.playList.findIndex((item: SongResult) => item.id === playMusic.id);
state.playListIndex = playListIndex;
// 请求后续五首歌曲的详情
fetchSongs(state, playListIndex + 1, playListIndex + 6);
};
const preloadingSounds = ref<Howl[]>([]);
// 用于预加载下一首歌曲的 MP3 数据
const preloadNextSong = (nextSongUrl: string) => {
try {
// 限制同时预加载的数量
if (preloadingSounds.value.length >= 2) {
const oldestSound = preloadingSounds.value.shift();
if (oldestSound) {
oldestSound.unload();
}
}
const sound = new Howl({
src: [nextSongUrl],
html5: true,
preload: true,
autoplay: false
});
preloadingSounds.value.push(sound);
// 添加加载错误处理
sound.on('loaderror', () => {
console.error('预加载音频失败:', nextSongUrl);
const index = preloadingSounds.value.indexOf(sound);
if (index > -1) {
preloadingSounds.value.splice(index, 1);
}
sound.unload();
});
return sound;
} catch (error) {
console.error('预加载音频出错:', error);
return null;
}
};
const fetchSongs = async (state: any, startIndex: number, endIndex: number) => {
try {
const songs = state.playList.slice(
Math.max(0, startIndex),
Math.min(endIndex, state.playList.length)
);
const detailedSongs = await Promise.all(
songs.map(async (song: SongResult) => {
try {
// 如果歌曲详情已经存在,就不重复请求
if (!song.playMusicUrl) {
return await getSongDetail(song);
}
return song;
} catch (error) {
console.error('获取歌曲详情失败:', error);
return song;
}
})
);
// 加载下一首的歌词
const nextSong = detailedSongs[0];
if (nextSong && !(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) {
try {
nextSong.lyric = await loadLrc(nextSong.id);
} catch (error) {
console.error('加载歌词失败:', error);
}
}
// 更新播放列表中的歌曲详情
detailedSongs.forEach((song, index) => {
if (song && startIndex + index < state.playList.length) {
state.playList[startIndex + index] = song;
}
});
// 只预加载下一首歌曲
if (nextSong && nextSong.playMusicUrl) {
preloadNextSong(nextSong.playMusicUrl);
}
} catch (error) {
console.error('获取歌曲列表失败:', error);
}
};
const nextPlay = async (state: any) => {
if (state.playList.length === 0) {
state.play = true;
return;
}
let playListIndex: number;
if (state.playMode === 2) {
// 随机播放模式
do {
playListIndex = Math.floor(Math.random() * state.playList.length);
} while (playListIndex === state.playListIndex && state.playList.length > 1);
} else {
// 列表循环模式
playListIndex = (state.playListIndex + 1) % state.playList.length;
}
state.playListIndex = playListIndex;
await handlePlayMusic(state, state.playList[playListIndex]);
};
const prevPlay = async (state: any) => {
if (state.playList.length === 0) {
state.play = true;
return;
}
const playListIndex = (state.playListIndex - 1 + state.playList.length) % state.playList.length;
await handlePlayMusic(state, state.playList[playListIndex]);
await fetchSongs(state, playListIndex - 5, playListIndex);
};
const parseTime = (timeString: string): number => {
const [minutes, seconds] = timeString.split(':');
return Number(minutes) * 60 + Number(seconds);
};
const parseLyricLine = (lyricLine: string): { time: number; text: string } => {
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
const time = parseTime(timeText);
const text = lyricLine.replace(LRC_REGEX, '').trim();
return { time, text };
};
const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => {
const lines = lyricsString.split('\n');
const lyrics: ILyricText[] = [];
const times: number[] = [];
lines.forEach((line) => {
const { time, text } = parseLyricLine(line);
times.push(time);
lyrics.push({ text, trText: '' });
});
return { lyrics, times };
};
const loadLrc = async (playMusicId: number): Promise<ILyric> => {
try {
const { data } = await getMusicLrc(playMusicId);
const { lyrics, times } = parseLyrics(data.lrc.lyric);
const tlyric: Record<string, string> = {};
if (data.tlyric && data.tlyric.lyric) {
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
tLyrics.forEach((lyric, index) => {
tlyric[tTimes[index].toString()] = lyric.text;
});
}
lyrics.forEach((item, index) => {
item.trText = item.text ? tlyric[times[index].toString()] || '' : '';
});
return {
lrcTimeArray: times,
lrcArray: lyrics
};
} catch (err) {
console.error('Error loading lyrics:', err);
return {
lrcTimeArray: [],
lrcArray: []
};
}
};
// 异步加载歌词的方法
const loadLrcAsync = async (state: any, playMusicId: any) => {
if (state.playMusic.lyric && state.playMusic.lyric.lrcTimeArray.length > 0) {
return;
}
const lyrics = await loadLrc(playMusicId);
state.playMusic.lyric = lyrics;
};
const play = () => {
audioService.getCurrentSound()?.play();
};
const pause = () => {
audioService.getCurrentSound()?.pause();
};
// 在组件卸载时清理预加载的音频
onUnmounted(() => {
preloadingSounds.value.forEach((sound) => sound.unload());
preloadingSounds.value = [];
});
return {
handlePlayMusic,
nextPlay,
prevPlay,
play,
pause
};
};

View File

@@ -14,6 +14,7 @@ import { type Platform } from '@/types/music';
import { getImgUrl } from '@/utils'; import { getImgUrl } from '@/utils';
import { hasPermission } from '@/utils/auth'; import { hasPermission } from '@/utils/auth';
import { getImageLinearBackground } from '@/utils/linearColor'; import { getImageLinearBackground } from '@/utils/linearColor';
import { parseLyrics as parseYrcLyrics } from '@/utils/yrcParser';
import { useSettingsStore } from './settings'; import { useSettingsStore } from './settings';
import { useUserStore } from './user'; import { useUserStore } from './user';
@@ -207,30 +208,56 @@ export const getSongUrl = async (
} }
}; };
const parseTime = (timeString: string): number => { /**
const [minutes, seconds] = timeString.split(':'); * 使用新的yrcParser解析歌词
return Number(minutes) * 60 + Number(seconds); * @param lyricsString 歌词字符串
}; * @returns 解析后的歌词数据
*/
const parseLyricLine = (lyricLine: string): { time: number; text: string } => {
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
const time = parseTime(timeText);
const text = lyricLine.replace(LRC_REGEX, '').trim();
return { time, text };
};
const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => { const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => {
const lines = lyricsString.split('\n'); if (!lyricsString || typeof lyricsString !== 'string') {
const lyrics: ILyricText[] = []; return { lyrics: [], times: [] };
const times: number[] = []; }
lines.forEach((line) => {
const { time, text } = parseLyricLine(line); try {
times.push(time); const parseResult = parseYrcLyrics(lyricsString);
lyrics.push({ text, trText: '' });
}); if (!parseResult.success) {
return { lyrics, times }; console.error('歌词解析失败:', parseResult.error.message);
return { lyrics: [], times: [] };
}
const { lyrics: parsedLyrics } = parseResult.data;
const lyrics: ILyricText[] = [];
const times: number[] = [];
for (const line of parsedLyrics) {
// 检查是否有逐字歌词
const hasWords = line.words && line.words.length > 0;
lyrics.push({
text: line.fullText,
trText: '', // 翻译文本稍后处理
words: hasWords
? line.words.map((word) => ({
text: word.text,
startTime: word.startTime,
duration: word.duration
}))
: undefined,
hasWordByWord: hasWords,
startTime: line.startTime,
duration: line.duration
});
// 时间数组使用秒为单位(与原有逻辑保持一致)
times.push(line.startTime / 1000);
}
return { lyrics, times };
} catch (error) {
console.error('解析歌词时发生错误:', error);
return { lyrics: [], times: [] };
}
}; };
export const loadLrc = async (id: string | number): Promise<ILyric> => { export const loadLrc = async (id: string | number): Promise<ILyric> => {
@@ -238,16 +265,26 @@ export const loadLrc = async (id: string | number): Promise<ILyric> => {
console.log('B站音频无需加载歌词'); console.log('B站音频无需加载歌词');
return { return {
lrcTimeArray: [], lrcTimeArray: [],
lrcArray: [] lrcArray: [],
hasWordByWord: false
}; };
} }
try { try {
const numericId = typeof id === 'string' ? parseInt(id, 10) : id; const numericId = typeof id === 'string' ? parseInt(id, 10) : id;
const { data } = await getMusicLrc(numericId); const { data } = await getMusicLrc(numericId);
const { lyrics, times } = parseLyrics(data.lrc.lyric); const { lyrics, times } = parseLyrics(data?.yrc?.lyric || data?.lrc?.lyric);
const tlyric: Record<string, string> = {}; const tlyric: Record<string, string> = {};
// 检查是否有逐字歌词
let hasWordByWord = false;
for (const lyric of lyrics) {
if (lyric.hasWordByWord) {
hasWordByWord = true;
break;
}
}
if (data.tlyric && data.tlyric.lyric) { if (data.tlyric && data.tlyric.lyric) {
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric); const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
tLyrics.forEach((lyric, index) => { tLyrics.forEach((lyric, index) => {
@@ -258,15 +295,18 @@ export const loadLrc = async (id: string | number): Promise<ILyric> => {
lyrics.forEach((item, index) => { lyrics.forEach((item, index) => {
item.trText = item.text ? tlyric[times[index].toString()] || '' : ''; item.trText = item.text ? tlyric[times[index].toString()] || '' : '';
}); });
return { return {
lrcTimeArray: times, lrcTimeArray: times,
lrcArray: lyrics lrcArray: lyrics,
hasWordByWord
}; };
} catch (err) { } catch (err) {
console.error('Error loading lyrics:', err); console.error('Error loading lyrics:', err);
return { return {
lrcTimeArray: [], lrcTimeArray: [],
lrcArray: [] lrcArray: [],
hasWordByWord: false
}; };
} }
}; };
@@ -275,7 +315,6 @@ const getSongDetail = async (playMusic: SongResult) => {
// playMusic.playLoading 在 handlePlayMusic 中已设置,这里不再设置 // playMusic.playLoading 在 handlePlayMusic 中已设置,这里不再设置
if (playMusic.source === 'bilibili') { if (playMusic.source === 'bilibili') {
console.log('处理B站音频详情');
try { try {
// 如果需要获取URL // 如果需要获取URL
if (!playMusic.playMusicUrl && playMusic.bilibiliData) { if (!playMusic.playMusicUrl && playMusic.bilibiliData) {

View File

@@ -9,13 +9,27 @@ export interface IRecommendMusic {
category: number; category: number;
result: SongResult[]; result: SongResult[];
} }
// 逐字歌词单词数据
export interface IWordData {
text: string;
startTime: number;
duration: number;
}
export interface ILyricText { export interface ILyricText {
text: string; text: string;
trText: string; trText: string;
words?: IWordData[];
hasWordByWord?: boolean;
startTime?: number;
duration?: number;
} }
export interface ILyric { export interface ILyric {
lrcTimeArray: number[]; lrcTimeArray: number[];
lrcArray: ILyricText[]; lrcArray: ILyricText[];
// 新增字段标识是否包含逐字歌词
hasWordByWord?: boolean;
} }
export interface SongResult { export interface SongResult {

View File

@@ -0,0 +1,388 @@
/**
* 歌词单词数据接口
*/
export interface WordData {
/** 单词文本内容 */
readonly text: string;
/** 开始时间(毫秒) */
readonly startTime: number;
/** 持续时间(毫秒) */
readonly duration: number;
}
/**
* 歌词行数据接口
*/
export interface LyricLine {
/** 行开始时间(毫秒) */
readonly startTime: number;
/** 行持续时间(毫秒) */
readonly duration: number;
/** 完整文本内容 */
readonly fullText: string;
/** 单词数组 */
readonly words: readonly WordData[];
}
/**
* 元数据接口
*/
export interface MetaData {
/** 时间戳 */
readonly time: number;
/** 内容 */
readonly content: string;
}
/**
* 解析结果接口
*/
export interface ParsedLyrics {
/** 元数据数组 */
readonly metadata: readonly MetaData[];
/** 歌词行数组 */
readonly lyrics: readonly LyricLine[];
}
/**
* 自定义解析错误类
*/
export class LyricParseError extends Error {
constructor(
message: string,
public readonly line?: string
) {
super(message);
this.name = 'LyricParseError';
}
}
/**
* 解析结果类型
*/
export type ParseResult<T> =
| { success: true; data: T }
| { success: false; error: LyricParseError };
// 预编译正则表达式以提高性能
const METADATA_PATTERN = /^\{"t":/;
const LINE_TIME_PATTERN = /^\[(\d+),(\d+)\](.+)$/; // 逐字歌词格式: [92260,4740]...
const LRC_TIME_PATTERN = /^\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)$/; // 标准LRC格式: [00:25.47]...
const WORD_PATTERN = /\((\d+),(\d+),\d+\)([^(]*?)(?=\(|$)/g;
/**
* 时间格式化函数
* @param ms 毫秒数
* @returns 格式化的时间字符串
*/
export const formatTime = (ms: number): string => {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const milliseconds = ms % 1000;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
};
/**
* 解析元数据行
* @param line 元数据行字符串
* @returns 解析结果
*/
const parseMetadata = (line: string): ParseResult<MetaData> => {
try {
const data = JSON.parse(line);
// 类型守卫:检查数据结构
if (typeof data !== 'object' || data === null) {
return {
success: false,
error: new LyricParseError('元数据格式无效:不是有效的对象', line)
};
}
if (typeof data.t !== 'number' || !Array.isArray(data.c)) {
return {
success: false,
error: new LyricParseError('元数据格式无效:缺少必要字段', line)
};
}
const content = data.c
.filter((item: any) => item && typeof item.tx === 'string')
.map((item: any) => item.tx)
.join('');
return {
success: true,
data: {
time: data.t,
content
}
};
} catch (error) {
return {
success: false,
error: new LyricParseError(
`JSON解析失败: ${error instanceof Error ? error.message : '未知错误'}`,
line
)
};
}
};
/**
* 解析标准LRC格式的歌词行
* @param line 歌词行字符串
* @returns 解析结果
*/
const parseLrcLine = (line: string): ParseResult<LyricLine> => {
const lrcMatch = line.match(LRC_TIME_PATTERN);
if (!lrcMatch) {
return {
success: false,
error: new LyricParseError('LRC歌词行格式无效无法匹配时间信息', line)
};
}
const minutes = parseInt(lrcMatch[1], 10);
const seconds = parseInt(lrcMatch[2], 10);
const milliseconds = parseInt(lrcMatch[3].padEnd(3, '0'), 10); // 处理2位或3位毫秒
const text = lrcMatch[4].trim();
// 验证时间值
if (
isNaN(minutes) ||
isNaN(seconds) ||
isNaN(milliseconds) ||
minutes < 0 ||
seconds < 0 ||
milliseconds < 0 ||
seconds >= 60
) {
return {
success: false,
error: new LyricParseError('LRC歌词行格式无效时间值无效', line)
};
}
const startTime = minutes * 60000 + seconds * 1000 + milliseconds;
return {
success: true,
data: {
startTime,
duration: 0, // LRC格式没有持续时间信息
fullText: text,
words: [] // LRC格式没有逐字信息
}
};
};
/**
* 解析逐字歌词行
* @param line 歌词行字符串
* @returns 解析结果
*/
const parseWordByWordLine = (line: string): ParseResult<LyricLine> => {
// 使用预编译的正则表达式
const lineTimeMatch = line.match(LINE_TIME_PATTERN);
if (!lineTimeMatch) {
return {
success: false,
error: new LyricParseError('逐字歌词行格式无效:无法匹配时间信息', line)
};
}
const startTime = parseInt(lineTimeMatch[1], 10);
const duration = parseInt(lineTimeMatch[2], 10);
const content = lineTimeMatch[3];
// 验证时间值
if (isNaN(startTime) || isNaN(duration) || startTime < 0 || duration < 0) {
return {
success: false,
error: new LyricParseError('逐字歌词行格式无效:时间值无效', line)
};
}
// 重置正则表达式状态
WORD_PATTERN.lastIndex = 0;
const words: WordData[] = [];
const textParts: string[] = [];
let match: RegExpExecArray | null;
// 使用exec而不是matchAll以更好地控制性能
while ((match = WORD_PATTERN.exec(content)) !== null) {
const wordStartTime = parseInt(match[1], 10);
const wordDuration = parseInt(match[2], 10);
const wordText = match[3].trim();
// 验证单词数据
if (isNaN(wordStartTime) || isNaN(wordDuration)) {
continue; // 跳过无效的单词数据
}
if (wordText) {
words.push({
text: wordText,
startTime: wordStartTime,
duration: wordDuration
});
textParts.push(wordText);
}
}
return {
success: true,
data: {
startTime,
duration,
fullText: textParts.join(' '),
words
}
};
};
/**
* 解析歌词行(自动检测格式)
* @param line 歌词行字符串
* @returns 解析结果
*/
const parseLyricLine = (line: string): ParseResult<LyricLine> => {
// 首先尝试解析逐字歌词格式
if (LINE_TIME_PATTERN.test(line)) {
return parseWordByWordLine(line);
}
// 然后尝试解析标准LRC格式
if (LRC_TIME_PATTERN.test(line)) {
return parseLrcLine(line);
}
return {
success: false,
error: new LyricParseError('歌词行格式无效:不匹配任何已知格式', line)
};
};
/**
* 计算LRC格式歌词的持续时间
* @param lyrics 歌词行数组
* @returns 更新持续时间后的歌词行数组
*/
const calculateLrcDurations = (lyrics: LyricLine[]): LyricLine[] => {
if (lyrics.length === 0) return lyrics;
const updatedLyrics: LyricLine[] = [];
for (let i = 0; i < lyrics.length; i++) {
const currentLine = lyrics[i];
// 如果已经有持续时间(逐字歌词),直接使用
if (currentLine.duration > 0) {
updatedLyrics.push(currentLine);
continue;
}
// 计算LRC格式的持续时间
let duration = 0;
if (i < lyrics.length - 1) {
// 使用下一行的开始时间减去当前行的开始时间
duration = lyrics[i + 1].startTime - currentLine.startTime;
} else {
// 最后一行使用默认持续时间3秒
duration = 3000;
}
// 确保持续时间不为负数
duration = Math.max(duration, 0);
updatedLyrics.push({
...currentLine,
duration
});
}
return updatedLyrics;
};
/**
* 主解析函数
* @param lyricsStr 歌词字符串
* @returns 解析结果
*/
export const parseLyrics = (lyricsStr: string): ParseResult<ParsedLyrics> => {
if (typeof lyricsStr !== 'string') {
return {
success: false,
error: new LyricParseError('输入参数必须是字符串')
};
}
try {
const lines = lyricsStr.trim().split('\n');
const metadata: MetaData[] = [];
const lyrics: LyricLine[] = [];
const errors: LyricParseError[] = [];
for (let i = 0; i < lines.length; i++) {
const trimmedLine = lines[i].trim();
if (!trimmedLine) continue;
// 使用预编译正则表达式进行快速检测
if (METADATA_PATTERN.test(trimmedLine)) {
const result = parseMetadata(trimmedLine);
if (result.success) {
metadata.push(result.data);
} else {
errors.push(result.error);
}
} else if (trimmedLine.startsWith('[')) {
const result = parseLyricLine(trimmedLine);
if (result.success) {
lyrics.push(result.data);
} else {
errors.push(result.error);
}
} else {
errors.push(new LyricParseError(`${i + 1}行:无法识别的行格式`, trimmedLine));
}
}
// 如果有太多错误,可能整个文件格式有问题
if (errors.length > 0 && errors.length > lines.length * 0.5) {
return {
success: false,
error: new LyricParseError(
`解析失败:错误行数过多 (${errors.length}/${lines.length}),可能文件格式不正确 ${JSON.stringify(errors)}`
)
};
}
// 按时间排序歌词行
lyrics.sort((a, b) => a.startTime - b.startTime);
// 计算LRC格式的持续时间
const finalLyrics = calculateLrcDurations(lyrics);
return {
success: true,
data: {
metadata,
lyrics: finalLyrics
}
};
} catch (error) {
return {
success: false,
error: new LyricParseError(
`解析过程中发生错误: ${error instanceof Error ? error.message : '未知错误'}`
)
};
}
};
/**
* 导出默认解析函数(向后兼容)
*/
export default parseLyrics;