mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-23 15:47:23 +08:00
✨ feat: 歌曲右键菜单添加下载歌词功能及下载设置中保存歌词文件选项
- 右键菜单新增"下载歌词"选项,支持获取歌词并保存为 .lrc 文件 - 如有翻译歌词会自动合并到 LRC 文件中 - 下载设置面板新增"单独保存歌词文件"开关 - 开启后下载歌曲时自动在同目录生成同名 .lrc 歌词文件 - 主进程新增 save-lyric-file IPC handler - 完成 5 种语言的国际化翻译
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
@play="playMusicEvent(item)"
|
||||
@play-next="handlePlayNext"
|
||||
@download="downloadMusic(item)"
|
||||
@download-lyric="downloadLyric(item)"
|
||||
@toggle-favorite="toggleFavorite"
|
||||
@toggle-dislike="toggleDislike"
|
||||
@remove="$emit('remove-song', $event)"
|
||||
@@ -71,7 +72,8 @@ const {
|
||||
handleArtistClick,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
downloadMusic
|
||||
downloadMusic,
|
||||
downloadLyric
|
||||
} = useSongItem(props);
|
||||
|
||||
// 处理图片加载
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
@play="onPlayMusic"
|
||||
@play-next="handlePlayNext"
|
||||
@download="downloadMusic"
|
||||
@download-lyric="downloadLyric(item)"
|
||||
@toggle-favorite="toggleFavorite"
|
||||
@toggle-dislike="toggleDislike"
|
||||
@remove="$emit('remove-song', $event)"
|
||||
@@ -121,7 +122,8 @@ const {
|
||||
handlePlayNext,
|
||||
handleMenuClick,
|
||||
handleArtistClick,
|
||||
downloadMusic
|
||||
downloadMusic,
|
||||
downloadLyric
|
||||
} = useSongItem(props);
|
||||
|
||||
const onPlayMusic = () => {
|
||||
|
||||
@@ -41,6 +41,7 @@ const emits = defineEmits([
|
||||
'play',
|
||||
'play-next',
|
||||
'download',
|
||||
'download-lyric',
|
||||
'add-to-playlist',
|
||||
'toggle-favorite',
|
||||
'toggle-dislike',
|
||||
@@ -153,6 +154,11 @@ const dropdownOptions = computed<MenuOption[]>(() => {
|
||||
key: 'download',
|
||||
icon: () => h('i', { class: 'iconfont ri-download-line' })
|
||||
},
|
||||
{
|
||||
label: t('songItem.menu.downloadLyric'),
|
||||
key: 'downloadLyric',
|
||||
icon: () => h('i', { class: 'iconfont ri-file-text-line' })
|
||||
},
|
||||
{
|
||||
label: t('songItem.menu.addToPlaylist'),
|
||||
key: 'addToPlaylist',
|
||||
@@ -203,6 +209,9 @@ const handleSelect = (key: string | number) => {
|
||||
case 'download':
|
||||
emits('download');
|
||||
break;
|
||||
case 'downloadLyric':
|
||||
emits('download-lyric');
|
||||
break;
|
||||
case 'playNext':
|
||||
emits('play-next');
|
||||
break;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMessage } from 'naive-ui';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { getMusicLrc } from '@/api/music';
|
||||
import { getSongUrl } from '@/store/modules/player';
|
||||
import type { SongResult } from '@/types/music';
|
||||
import { isElectron } from '@/utils';
|
||||
@@ -302,9 +303,91 @@ export const useDownload = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载单首歌曲的歌词(.lrc 文件)
|
||||
* @param song 歌曲信息
|
||||
*/
|
||||
const downloadLyric = async (song: SongResult) => {
|
||||
try {
|
||||
const res = await getMusicLrc(song.id as number);
|
||||
const lyricData = res?.data;
|
||||
|
||||
if (!lyricData?.lrc?.lyric) {
|
||||
message.warning(t('songItem.message.noLyric'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建 LRC 内容:保留原始歌词,如有翻译则合并
|
||||
let lrcContent = lyricData.lrc.lyric;
|
||||
if (lyricData.tlyric?.lyric) {
|
||||
lrcContent = mergeLrcWithTranslation(lyricData.lrc.lyric, lyricData.tlyric.lyric);
|
||||
}
|
||||
|
||||
// 构建文件名
|
||||
const artistNames = (song.ar || song.song?.artists)?.map((a) => a.name).join(',');
|
||||
const filename = `${song.name} - ${artistNames}`;
|
||||
|
||||
const result = await ipcRenderer?.invoke('save-lyric-file', { filename, lrcContent });
|
||||
|
||||
if (result?.success) {
|
||||
message.success(t('songItem.message.lyricDownloaded'));
|
||||
} else {
|
||||
message.error(t('songItem.message.lyricDownloadFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download lyric error:', error);
|
||||
message.error(t('songItem.message.lyricDownloadFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isDownloading,
|
||||
downloadMusic,
|
||||
downloadLyric,
|
||||
batchDownloadMusic
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 将原文歌词和翻译歌词合并为一个 LRC 字符串
|
||||
*/
|
||||
function mergeLrcWithTranslation(originalText: string, translationText: string): string {
|
||||
const originalMap = parseLrcText(originalText);
|
||||
const translationMap = parseLrcText(translationText);
|
||||
|
||||
const mergedLines: string[] = [];
|
||||
|
||||
for (const [timeTag, content] of originalMap.entries()) {
|
||||
mergedLines.push(`${timeTag}${content}`);
|
||||
const translated = translationMap.get(timeTag);
|
||||
if (translated) {
|
||||
mergedLines.push(`${timeTag}${translated}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
mergedLines.sort((a, b) => {
|
||||
const ta = a.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/)?.[0] || '';
|
||||
const tb = b.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/)?.[0] || '';
|
||||
return ta.localeCompare(tb);
|
||||
});
|
||||
|
||||
return mergedLines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 LRC 文本为 Map<timeTag, content>
|
||||
*/
|
||||
function parseLrcText(text: string): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
for (const line of text.split('\n')) {
|
||||
const tags = line.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/g);
|
||||
if (!tags) continue;
|
||||
const content = line.replace(/\[\d{2}:\d{2}(\.\d{1,3})?\]/g, '').trim();
|
||||
if (!content) continue;
|
||||
for (const tag of tags) {
|
||||
map.set(tag, content);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function useSongItem(props: { item: SongResult; canRemove?: boolean }) {
|
||||
const playerStore = usePlayerStore();
|
||||
const recommendStore = useRecommendStore();
|
||||
const message = useMessage();
|
||||
const { downloadMusic } = useDownload();
|
||||
const { downloadMusic, downloadLyric } = useDownload();
|
||||
const { navigateToArtist } = useArtist();
|
||||
|
||||
// 状态变量
|
||||
@@ -220,6 +220,7 @@ export function useSongItem(props: { item: SongResult; canRemove?: boolean }) {
|
||||
handleArtistClick,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
downloadMusic
|
||||
downloadMusic,
|
||||
downloadLyric
|
||||
};
|
||||
}
|
||||
|
||||
@@ -316,6 +316,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Lyric File -->
|
||||
<div class="setting-group">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-neutral-900 dark:text-white">
|
||||
{{ t('download.settingsPanel.saveLyric') }}
|
||||
</h3>
|
||||
<p class="text-xs text-neutral-500 mt-1">
|
||||
{{ t('download.settingsPanel.saveLyricDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<n-switch v-model:value="downloadSettings.saveLyric" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Format Section -->
|
||||
<div class="setting-group">
|
||||
<h3 class="text-sm font-bold text-neutral-900 dark:text-white mb-2">
|
||||
@@ -875,7 +890,8 @@ const showSettingsDrawer = ref(false);
|
||||
const downloadSettings = ref({
|
||||
path: '',
|
||||
nameFormat: '{songName} - {artistName}',
|
||||
separator: ' - '
|
||||
separator: ' - ',
|
||||
saveLyric: false
|
||||
});
|
||||
|
||||
// 格式组件(用于拖拽排序)
|
||||
@@ -992,6 +1008,11 @@ const saveDownloadSettings = () => {
|
||||
'set.downloadSeparator',
|
||||
downloadSettings.value.separator
|
||||
);
|
||||
window.electron.ipcRenderer.send(
|
||||
'set-store-value',
|
||||
'set.downloadSaveLyric',
|
||||
downloadSettings.value.saveLyric
|
||||
);
|
||||
|
||||
// 如果是在已下载页面,刷新列表以更新显示
|
||||
if (tabName.value === 'downloaded') {
|
||||
@@ -1014,11 +1035,16 @@ const initDownloadSettings = async () => {
|
||||
'get-store-value',
|
||||
'set.downloadSeparator'
|
||||
);
|
||||
const saveLyric = await window.electron.ipcRenderer.invoke(
|
||||
'get-store-value',
|
||||
'set.downloadSaveLyric'
|
||||
);
|
||||
|
||||
downloadSettings.value = {
|
||||
path: path || (await window.electron.ipcRenderer.invoke('get-downloads-path')),
|
||||
nameFormat: nameFormat || '{songName} - {artistName}',
|
||||
separator: separator || ' - '
|
||||
separator: separator || ' - ',
|
||||
saveLyric: saveLyric || false
|
||||
};
|
||||
|
||||
// 初始化排序组件
|
||||
|
||||
Reference in New Issue
Block a user