From 3b1488f14708b75a267ebbb81a4f8f10ba256653 Mon Sep 17 00:00:00 2001 From: alger Date: Thu, 10 Apr 2025 00:26:58 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=AD=8C=E6=9B=B2=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=86=85=E7=BD=AE=E5=B0=81=E9=9D=A2=E6=AD=8C=E8=AF=8D?= =?UTF-8?q?=E6=AD=8C=E6=9B=B2=E4=BF=A1=E6=81=AF=E7=AD=89=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=97=A0=E9=99=90=E5=88=B6=E4=B8=8B=E8=BD=BD=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E4=B8=8B=E8=BD=BD=E7=AE=A1?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E6=94=AF=E6=8C=81=E6=B8=85=E7=A9=BA=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/i18n/lang/en-US/download.ts | 14 +- src/i18n/lang/en-US/settings.ts | 2 + src/i18n/lang/en-US/songItem.ts | 2 +- src/i18n/lang/zh-CN/download.ts | 13 +- src/i18n/lang/zh-CN/settings.ts | 2 + src/i18n/lang/zh-CN/songItem.ts | 2 +- src/main/modules/fileManager.ts | 219 +++++++++++++++++- src/main/set.json | 3 +- .../components/common/DownloadDrawer.vue | 98 ++++++-- src/renderer/components/common/SongItem.vue | 15 +- src/renderer/hooks/MusicListHook.ts | 9 +- src/renderer/views/favorite/index.vue | 11 +- src/renderer/views/set/index.vue | 13 ++ 14 files changed, 370 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 277399e..38c3e7b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "electron-updater": "^6.1.7", "font-list": "^1.5.1", "netease-cloud-music-api-alger": "^4.26.1", + "node-id3": "^0.2.9", "vue-i18n": "9" }, "devDependencies": { diff --git a/src/i18n/lang/en-US/download.ts b/src/i18n/lang/en-US/download.ts index e688cf9..61b6baf 100644 --- a/src/i18n/lang/en-US/download.ts +++ b/src/i18n/lang/en-US/download.ts @@ -1,6 +1,8 @@ export default { title: 'Download Manager', localMusic: 'Local Music', + count: '{count} songs in total', + clearAll: 'Clear All', tabs: { downloading: 'Downloading', downloaded: 'Downloaded' @@ -27,7 +29,17 @@ export default { confirm: 'Delete', cancel: 'Cancel', success: 'Successfully deleted', - failed: 'Failed to delete' + failed: 'Failed to delete', + fileNotFound: 'File not found or moved, removed from records', + recordRemoved: 'Failed to delete file, but removed from records' + }, + clear: { + title: 'Clear Download Records', + message: + 'Are you sure you want to clear all download records? This will not delete the actual music files, but will clear all records.', + confirm: 'Clear', + cancel: 'Cancel', + success: 'Download records cleared' }, message: { downloadComplete: '{filename} download completed', diff --git a/src/i18n/lang/en-US/settings.ts b/src/i18n/lang/en-US/settings.ts index b7c7bf9..2b64b18 100644 --- a/src/i18n/lang/en-US/settings.ts +++ b/src/i18n/lang/en-US/settings.ts @@ -71,6 +71,8 @@ export default { shortcutDesc: 'Customize global shortcuts', download: 'Download Management', downloadDesc: 'Always show download list button', + unlimitedDownload: 'Unlimited Download', + unlimitedDownloadDesc: 'Enable unlimited download mode for music , default limit 300 songs', downloadPath: 'Download Directory', downloadPathDesc: 'Choose download location for music files' }, diff --git a/src/i18n/lang/en-US/songItem.ts b/src/i18n/lang/en-US/songItem.ts index 7cfc2d7..f6e5bb2 100644 --- a/src/i18n/lang/en-US/songItem.ts +++ b/src/i18n/lang/en-US/songItem.ts @@ -13,6 +13,6 @@ export default { downloadFailed: 'Download failed', downloadQueued: 'Added to download queue', addedToNextPlay: 'Added to play next', - getUrlFailed: 'Failed to get music download URL' + getUrlFailed: 'Failed to get music download URL, please check if logged in' } }; diff --git a/src/i18n/lang/zh-CN/download.ts b/src/i18n/lang/zh-CN/download.ts index 7199964..be06a45 100644 --- a/src/i18n/lang/zh-CN/download.ts +++ b/src/i18n/lang/zh-CN/download.ts @@ -1,6 +1,8 @@ export default { title: '下载管理', localMusic: '本地音乐', + count: '共 {count} 首歌曲', + clearAll: '清空记录', tabs: { downloading: '下载中', downloaded: '已下载' @@ -27,7 +29,16 @@ export default { confirm: '确定删除', cancel: '取消', success: '删除成功', - failed: '删除失败' + failed: '删除失败', + fileNotFound: '文件不存在或已被移动,已从记录中移除', + recordRemoved: '文件删除失败,但已从记录中移除' + }, + clear: { + title: '清空下载记录', + message: '确定要清空所有下载记录吗?此操作不会删除已下载的音乐文件,但将清空所有记录。', + confirm: '确定清空', + cancel: '取消', + success: '下载记录已清空' }, message: { downloadComplete: '{filename} 下载完成', diff --git a/src/i18n/lang/zh-CN/settings.ts b/src/i18n/lang/zh-CN/settings.ts index def504f..49b4534 100644 --- a/src/i18n/lang/zh-CN/settings.ts +++ b/src/i18n/lang/zh-CN/settings.ts @@ -71,6 +71,8 @@ export default { shortcutDesc: '自定义全局快捷键', download: '下载管理', downloadDesc: '是否始终显示下载列表按钮', + unlimitedDownload: '无限制下载', + unlimitedDownloadDesc: '开启后将无限制下载音乐(可能出现下载失败的情况), 默认限制 300 首', downloadPath: '下载目录', downloadPathDesc: '选择音乐文件的下载位置' }, diff --git a/src/i18n/lang/zh-CN/songItem.ts b/src/i18n/lang/zh-CN/songItem.ts index 7c90b36..b9339f5 100644 --- a/src/i18n/lang/zh-CN/songItem.ts +++ b/src/i18n/lang/zh-CN/songItem.ts @@ -13,6 +13,6 @@ export default { downloadFailed: '下载失败', downloadQueued: '已加入下载队列', addedToNextPlay: '已添加到下一首播放', - getUrlFailed: '获取音乐下载地址失败' + getUrlFailed: '获取音乐下载地址失败,请检查是否登录' } }; diff --git a/src/main/modules/fileManager.ts b/src/main/modules/fileManager.ts index 3fa061b..2d2489e 100644 --- a/src/main/modules/fileManager.ts +++ b/src/main/modules/fileManager.ts @@ -1,9 +1,14 @@ import axios from 'axios'; -import { app, dialog, ipcMain, protocol, shell } from 'electron'; +import { app, dialog, ipcMain, Notification, protocol, shell } from 'electron'; import Store from 'electron-store'; import * as fs from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import * as NodeID3 from 'node-id3'; import * as path from 'path'; +import { getStore } from './config'; + const MAX_CONCURRENT_DOWNLOADS = 3; const downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = []; let activeDownloads = 0; @@ -269,7 +274,7 @@ function sanitizeFilename(filename: string): string { } /** - * 下载音乐功能 + * 下载音乐和歌词 */ async function downloadMusic( event: Electron.IpcMainEvent, @@ -284,8 +289,11 @@ async function downloadMusic( let writer: fs.WriteStream | null = null; try { - const store = new Store(); - const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads'); + // 使用配置Store来获取设置 + const configStore = getStore(); + const downloadPath = + (configStore.get('set.downloadPath') as string) || app.getPath('downloads'); + const apiPort = configStore.get('set.musicApiPort') || 30488; // 清理文件名中的非法字符 const sanitizedFilename = sanitizeFilename(filename); @@ -313,7 +321,9 @@ async function downloadMusic( url, method: 'GET', responseType: 'stream', - timeout: 30000 // 30秒超时 + timeout: 30000, // 30秒超时 + httpAgent: new http.Agent({ keepAlive: true }), + httpsAgent: new https.Agent({ keepAlive: true }) }); writer = fs.createWriteStream(finalFilePath); @@ -351,9 +361,121 @@ async function downloadMusic( throw new Error('文件下载不完整'); } + // 下载歌词 + let lyricData = null; + let lyricsContent = ''; + try { + if (songInfo?.id) { + // 下载歌词,使用配置的端口 + const lyricsResponse = await axios.get( + `http://localhost:${apiPort}/lyric?id=${songInfo.id}` + ); + if (lyricsResponse.data && (lyricsResponse.data.lrc || lyricsResponse.data.tlyric)) { + lyricData = lyricsResponse.data; + + // 处理歌词内容 + if (lyricsResponse.data.lrc && lyricsResponse.data.lrc.lyric) { + lyricsContent = lyricsResponse.data.lrc.lyric; + + // 如果有翻译歌词,合并到主歌词中 + if (lyricsResponse.data.tlyric && lyricsResponse.data.tlyric.lyric) { + // 解析原歌词和翻译 + const originalLyrics = parseLyrics(lyricsResponse.data.lrc.lyric); + const translatedLyrics = parseLyrics(lyricsResponse.data.tlyric.lyric); + + // 合并歌词 + const mergedLyrics = mergeLyrics(originalLyrics, translatedLyrics); + lyricsContent = mergedLyrics; + } + } + + // 不再单独写入歌词文件,只保存在ID3标签中 + console.log('歌词已准备好,将写入ID3标签'); + } + } + } catch (lyricError) { + console.error('下载歌词失败:', lyricError); + // 继续处理,不影响音乐下载 + } + + // 下载封面 + let coverImageBuffer: Buffer | null = null; + try { + if (songInfo?.picUrl || songInfo?.al?.picUrl) { + const picUrl = songInfo.picUrl || songInfo.al?.picUrl; + if (picUrl && picUrl !== '/images/default_cover.png') { + const coverResponse = await axios({ + url: picUrl.replace('http://', 'https://'), + method: 'GET', + responseType: 'arraybuffer', + timeout: 10000 + }); + + // 获取封面图片的buffer + coverImageBuffer = Buffer.from(coverResponse.data); + + // 不再单独保存封面文件,只保存在ID3标签中 + console.log('封面已准备好,将写入ID3标签'); + } + } + } catch (coverError) { + console.error('下载封面失败:', coverError); + // 继续处理,不影响音乐下载 + } + + // 在写入ID3标签前,先移除可能存在的旧标签 + try { + NodeID3.removeTags(finalFilePath); + } catch (err) { + console.error('Error removing existing ID3 tags:', err); + } + + // 强化ID3标签的写入格式 + + const artistNames = + (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/ ') || '未知艺术家'; + const tags = { + title: filename, + artist: artistNames, + TPE1: artistNames, + TPE2: artistNames, + album: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename, + APIC: { + // 专辑封面 + imageBuffer: coverImageBuffer, + type: { + id: 3, + name: 'front cover' + }, + description: 'Album cover', + mime: 'image/jpeg' + }, + USLT: { + // 歌词 + language: 'chi', + description: 'Lyrics', + text: lyricsContent || '' + }, + trackNumber: songInfo?.no || undefined, + year: songInfo?.publishTime + ? new Date(songInfo.publishTime).getFullYear().toString() + : undefined + }; + + try { + const success = NodeID3.write(tags, finalFilePath); + if (!success) { + console.error('Failed to write ID3 tags'); + } else { + console.log('ID3 tags written successfully'); + } + } catch (err) { + console.error('Error writing ID3 tags:', err); + } + // 保存下载信息 try { - const songInfos = store.get('downloadedSongs', {}) as Record; + const songInfos = configStore.get('downloadedSongs', {}) as Record; const defaultInfo = { name: filename, ar: [{ name: '本地音乐' }], @@ -364,24 +486,48 @@ async function downloadMusic( id: songInfo?.id || 0, name: songInfo?.name || filename, filename, - picUrl: songInfo?.picUrl || defaultInfo.picUrl, + picUrl: songInfo?.picUrl || songInfo?.al?.picUrl || defaultInfo.picUrl, ar: songInfo?.ar || defaultInfo.ar, + al: songInfo?.al || { + picUrl: songInfo?.picUrl || defaultInfo.picUrl, + name: songInfo?.name || filename + }, size: totalSize, path: finalFilePath, downloadTime: Date.now(), - al: songInfo?.al || { picUrl: songInfo?.picUrl || defaultInfo.picUrl }, - type: type || 'mp3' + type: type || 'mp3', + lyric: lyricData }; // 保存到下载记录 songInfos[finalFilePath] = newSongInfo; - store.set('downloadedSongs', songInfos); + configStore.set('downloadedSongs', songInfos); // 添加到下载历史 const history = downloadStore.get('history', []) as any[]; history.unshift(newSongInfo); downloadStore.set('history', history); + // 发送桌面通知 + try { + const artistNames = + (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/') || + '未知艺术家'; + const notification = new Notification({ + title: '下载完成', + body: `${songInfo?.name || filename} - ${artistNames}`, + silent: false + }); + + notification.on('click', () => { + shell.showItemInFolder(finalFilePath); + }); + + notification.show(); + } catch (notifyError) { + console.error('发送通知失败:', notifyError); + } + // 发送下载完成事件 event.reply('music-download-complete', { success: true, @@ -416,3 +562,56 @@ async function downloadMusic( }); } } + +// 辅助函数 - 解析歌词文本成时间戳和内容的映射 +function parseLyrics(lyricsText: string): Map { + const lyricMap = new Map(); + const lines = lyricsText.split('\n'); + + for (const line of lines) { + // 匹配时间标签,形如 [00:00.000] + const timeTagMatches = line.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/g); + if (!timeTagMatches) continue; + + // 提取歌词内容(去除时间标签) + const content = line.replace(/\[\d{2}:\d{2}(\.\d{1,3})?\]/g, '').trim(); + if (!content) continue; + + // 将每个时间标签与歌词内容关联 + for (const timeTag of timeTagMatches) { + lyricMap.set(timeTag, content); + } + } + + return lyricMap; +} + +// 辅助函数 - 合并原文歌词和翻译歌词 +function mergeLyrics( + originalLyrics: Map, + translatedLyrics: Map +): string { + const mergedLines: string[] = []; + + // 对每个时间戳,组合原始歌词和翻译 + for (const [timeTag, originalContent] of originalLyrics.entries()) { + const translatedContent = translatedLyrics.get(timeTag); + + // 添加原始歌词行 + mergedLines.push(`${timeTag}${originalContent}`); + + // 如果有翻译,添加翻译行(时间戳相同,这样可以和原歌词同步显示) + if (translatedContent) { + mergedLines.push(`${timeTag}${translatedContent}`); + } + } + + // 按时间顺序排序 + mergedLines.sort((a, b) => { + const timeA = a.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/)?.[0] || ''; + const timeB = b.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/)?.[0] || ''; + return timeA.localeCompare(timeB); + }); + + return mergedLines.join('\n'); +} diff --git a/src/main/set.json b/src/main/set.json index e6a70aa..cdfd8d2 100644 --- a/src/main/set.json +++ b/src/main/set.json @@ -20,5 +20,6 @@ "autoPlay": false, "downloadPath": "", "language": "zh-CN", - "alwaysShowDownloadButton": false + "alwaysShowDownloadButton": false, + "unlimitedDownload": false } diff --git a/src/renderer/components/common/DownloadDrawer.vue b/src/renderer/components/common/DownloadDrawer.vue index dfbd241..f659dce 100644 --- a/src/renderer/components/common/DownloadDrawer.vue +++ b/src/renderer/components/common/DownloadDrawer.vue @@ -94,6 +94,17 @@
+
+
+ {{ t('download.count', { count: downloadedList.length }) }} +
+ + + {{ t('download.clearAll') }} + +
@@ -172,12 +183,38 @@ }} + + + + +
+ {{ t('download.clear.message') }} +
+ +