From b203077cad1182e0929197b1c213fad09e5b8487 Mon Sep 17 00:00:00 2001 From: alger Date: Thu, 5 Jun 2025 23:02:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E6=96=87=E4=BB=B6=E5=90=8D=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=92=8C=E4=B8=8B=E8=BD=BD=E8=B7=AF=E5=BE=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增下载设置抽屉,允许用户设置下载路径和文件名格式 - 支持多种文件名格式预设和自定义格式 - 实现下载项的显示名称格式化 - 优化下载管理逻辑,避免重复通知 --- src/i18n/lang/en-US/download.ts | 40 +- src/i18n/lang/zh-CN/download.ts | 33 ++ src/main/modules/fileManager.ts | 90 +++- src/renderer/hooks/useDownload.ts | 226 +++++++-- src/renderer/views/download/DownloadPage.vue | 496 +++++++++++++++++-- 5 files changed, 784 insertions(+), 101 deletions(-) diff --git a/src/i18n/lang/en-US/download.ts b/src/i18n/lang/en-US/download.ts index 61d256d..03ffdc1 100644 --- a/src/i18n/lang/en-US/download.ts +++ b/src/i18n/lang/en-US/download.ts @@ -3,17 +3,20 @@ export default { localMusic: 'Local Music', count: '{count} songs in total', clearAll: 'Clear All', + settings: 'Settings', tabs: { downloading: 'Downloading', downloaded: 'Downloaded' }, empty: { noTasks: 'No download tasks', - noDownloaded: 'No downloaded songs' + noDownloaded: 'No downloaded songs', + noDownloadedHint: 'Download your favorite songs to listen offline' }, progress: { total: 'Total Progress: {progress}%' }, + items: 'items', status: { downloading: 'Downloading', completed: 'Completed', @@ -43,7 +46,8 @@ export default { }, message: { downloadComplete: '{filename} download completed', - downloadFailed: '{filename} download failed: {error}' + downloadFailed: '{filename} download failed: {error}', + alreadyDownloading: '{filename} is already downloading' }, loading: 'Loading...', playStarted: 'Play started: {name}', @@ -51,5 +55,37 @@ export default { path: { copied: 'Path copied to clipboard', copyFailed: 'Failed to copy path' + }, + settingsPanel: { + title: 'Download Settings', + path: 'Download Location', + pathDesc: 'Set where your music files will be saved', + pathPlaceholder: 'Please select download path', + noPathSelected: 'Please select download path first', + select: 'Select Folder', + open: 'Open Folder', + fileFormat: 'Filename Format', + fileFormatDesc: 'Set how downloaded music files will be named', + customFormat: 'Custom Format', + separator: 'Separator', + separators: { + dash: 'Space-dash-space', + underscore: 'Underscore', + space: 'Space' + }, + dragToArrange: 'Sort or use arrow buttons to arrange:', + formatVariables: 'Available variables', + preview: 'Preview:', + saveSuccess: 'Download settings saved', + presets: { + songArtist: 'Song - Artist', + artistSong: 'Artist - Song', + songOnly: 'Song only' + }, + components: { + songName: 'Song name', + artistName: 'Artist name', + albumName: 'Album name' + } } }; diff --git a/src/i18n/lang/zh-CN/download.ts b/src/i18n/lang/zh-CN/download.ts index 8996cfc..5753eb2 100644 --- a/src/i18n/lang/zh-CN/download.ts +++ b/src/i18n/lang/zh-CN/download.ts @@ -3,6 +3,7 @@ export default { localMusic: '本地音乐', count: '共 {count} 首歌曲', clearAll: '清空记录', + settings: '设置', tabs: { downloading: '下载中', downloaded: '已下载' @@ -50,5 +51,37 @@ export default { path: { copied: '路径已复制到剪贴板', copyFailed: '复制路径失败' + }, + settingsPanel: { + title: '下载设置', + path: '下载位置', + pathDesc: '设置音乐文件下载保存的位置', + pathPlaceholder: '请选择下载路径', + noPathSelected: '请先选择下载路径', + select: '选择文件夹', + open: '打开文件夹', + fileFormat: '文件名格式', + fileFormatDesc: '设置下载音乐时的文件命名格式', + customFormat: '自定义格式', + separator: '分隔符', + separators: { + dash: '空格-空格', + underscore: '下划线', + space: '空格' + }, + dragToArrange: '拖动排序或使用箭头按钮调整顺序:', + formatVariables: '可用变量', + preview: '预览效果:', + saveSuccess: '下载设置已保存', + presets: { + songArtist: '歌曲名 - 歌手名', + artistSong: '歌手名 - 歌曲名', + songOnly: '仅歌曲名' + }, + components: { + songName: '歌曲名', + artistName: '歌手名', + albumName: '专辑名' + } } }; diff --git a/src/main/modules/fileManager.ts b/src/main/modules/fileManager.ts index 2162e5d..8ab399a 100644 --- a/src/main/modules/fileManager.ts +++ b/src/main/modules/fileManager.ts @@ -34,6 +34,9 @@ const audioCacheStore = new Store({ } }); +// 保存已发送通知的文件,避免重复通知 +const sentNotifications = new Map(); + /** * 初始化文件管理相关的IPC监听 */ @@ -123,6 +126,23 @@ export function initializeFileManager() { } }); + // 获取默认下载路径 + ipcMain.handle('get-downloads-path', () => { + return app.getPath('downloads'); + }); + + // 获取存储的配置值 + ipcMain.handle('get-store-value', (_, key) => { + const store = new Store(); + return store.get(key); + }); + + // 设置存储的配置值 + ipcMain.on('set-store-value', (_, key, value) => { + const store = new Store(); + store.set(key, value); + }); + // 下载音乐处理 ipcMain.on('download-music', handleDownloadRequest); @@ -358,9 +378,28 @@ async function downloadMusic( const downloadPath = (configStore.get('set.downloadPath') as string) || app.getPath('downloads'); const apiPort = configStore.get('set.musicApiPort') || 30488; + + // 获取文件名格式设置 + const nameFormat = + (configStore.get('set.downloadNameFormat') as string) || '{songName} - {artistName}'; + + // 根据格式创建文件名 + let formattedFilename = filename; + if (songInfo) { + // 准备替换变量 + const artistName = songInfo.ar?.map((a: any) => a.name).join('/') || '未知艺术家'; + const songName = songInfo.name || filename; + const albumName = songInfo.al?.name || '未知专辑'; + + // 应用自定义格式 + formattedFilename = nameFormat + .replace(/\{songName\}/g, songName) + .replace(/\{artistName\}/g, artistName) + .replace(/\{albumName\}/g, albumName); + } // 清理文件名中的非法字符 - const sanitizedFilename = sanitizeFilename(filename); + const sanitizedFilename = sanitizeFilename(formattedFilename); // 创建临时文件路径 (在系统临时目录中创建) const tempDir = path.join(os.tmpdir(), 'AlgerMusicPlayerTemp'); @@ -635,27 +674,38 @@ async function downloadMusic( 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); + // 避免重复发送通知 + const notificationId = `download-${finalFilePath}`; + if (!sentNotifications.has(notificationId)) { + sentNotifications.set(notificationId, true); + + // 发送桌面通知 + 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(); + + // 60秒后清理通知记录,释放内存 + setTimeout(() => { + sentNotifications.delete(notificationId); + }, 60000); + } catch (notifyError) { + console.error('发送通知失败:', notifyError); + } } - // 发送下载完成事件 + // 发送下载完成事件,确保只发送一次 event.reply('music-download-complete', { success: true, path: finalFilePath, diff --git a/src/renderer/hooks/useDownload.ts b/src/renderer/hooks/useDownload.ts index bc8f7c9..61d61d6 100644 --- a/src/renderer/hooks/useDownload.ts +++ b/src/renderer/hooks/useDownload.ts @@ -6,11 +6,145 @@ import { useMessage } from 'naive-ui'; import { getSongUrl } from '@/store/modules/player'; import type { SongResult } from '@/type/music'; +// 全局下载管理(闭包模式) +const createDownloadManager = () => { + // 正在下载的文件集合 + const activeDownloads = new Set(); + + // 已经发送了通知的文件集合(避免重复通知) + const notifiedDownloads = new Set(); + + // 事件监听器是否已初始化 + let isInitialized = false; + + // 监听器引用(用于清理) + let completeListener: ((event: any, data: any) => void) | null = null; + let errorListener: ((event: any, data: any) => void) | null = null; + + return { + // 添加下载 + addDownload: (filename: string) => { + activeDownloads.add(filename); + }, + + // 移除下载 + removeDownload: (filename: string) => { + activeDownloads.delete(filename); + // 延迟清理通知记录 + setTimeout(() => { + notifiedDownloads.delete(filename); + }, 5000); + }, + + // 标记文件已通知 + markNotified: (filename: string) => { + notifiedDownloads.add(filename); + }, + + // 检查文件是否已通知 + isNotified: (filename: string) => { + return notifiedDownloads.has(filename); + }, + + // 清理所有下载 + clearDownloads: () => { + activeDownloads.clear(); + notifiedDownloads.clear(); + }, + + // 初始化事件监听器 + initEventListeners: (message: any, t: any) => { + if (isInitialized) return; + + // 移除可能存在的旧监听器 + if (completeListener) { + window.electron.ipcRenderer.removeListener('music-download-complete', completeListener); + } + + if (errorListener) { + window.electron.ipcRenderer.removeListener('music-download-error', errorListener); + } + + // 创建新的监听器 + completeListener = (_event, data) => { + if (!data.filename || !activeDownloads.has(data.filename)) return; + + // 如果该文件已经通知过,则跳过 + if (notifiedDownloads.has(data.filename)) return; + + // 标记为已通知 + notifiedDownloads.add(data.filename); + + // 从活动下载移除 + activeDownloads.delete(data.filename); + }; + + errorListener = (_event, data) => { + if (!data.filename || !activeDownloads.has(data.filename)) return; + + // 如果该文件已经通知过,则跳过 + if (notifiedDownloads.has(data.filename)) return; + + // 标记为已通知 + notifiedDownloads.add(data.filename); + + // 显示失败通知 + message.error(t('songItem.message.downloadFailed', { + filename: data.filename, + error: data.error || '未知错误' + })); + + // 从活动下载移除 + activeDownloads.delete(data.filename); + }; + + // 添加监听器 + window.electron.ipcRenderer.on('music-download-complete', completeListener); + window.electron.ipcRenderer.on('music-download-error', errorListener); + + isInitialized = true; + }, + + // 清理事件监听器 + cleanupEventListeners: () => { + if (!isInitialized) return; + + if (completeListener) { + window.electron.ipcRenderer.removeListener('music-download-complete', completeListener); + completeListener = null; + } + + if (errorListener) { + window.electron.ipcRenderer.removeListener('music-download-error', errorListener); + errorListener = null; + } + + isInitialized = false; + }, + + // 获取活跃下载数量 + getActiveDownloadCount: () => { + return activeDownloads.size; + }, + + // 检查是否有特定文件正在下载 + hasDownload: (filename: string) => { + return activeDownloads.has(filename); + } + }; +}; + +// 创建单例下载管理器 +const downloadManager = createDownloadManager(); + export const useDownload = () => { const { t } = useI18n(); const message = useMessage(); const isDownloading = ref(false); + // 初始化事件监听器 + downloadManager.initEventListeners(message, t); + /** * 下载单首音乐 * @param song 歌曲信息 @@ -33,9 +167,19 @@ export const useDownload = () => { // 构建文件名 const artistNames = (song.ar || song.song?.artists)?.map((a) => a.name).join(','); const filename = `${song.name} - ${artistNames}`; + + // 检查是否已在下载 + if (downloadManager.hasDownload(filename)) { + isDownloading.value = false; + return; + } + + // 添加到活动下载集合 + downloadManager.addDownload(filename); const songData = cloneDeep(song); songData.ar = songData.ar || songData.song?.artists; + // 发送下载请求 window.electron.ipcRenderer.send('download-music', { url: typeof musicUrl === 'string' ? musicUrl : musicUrl.url, @@ -43,39 +187,16 @@ export const useDownload = () => { songInfo: { ...songData, downloadTime: Date.now() - } + }, + type: musicUrl.type }); message.success(t('songItem.message.downloadQueued')); - - // 监听下载完成事件 - const handleDownloadComplete = (_, result) => { - if (result.filename === filename) { - isDownloading.value = false; - removeListeners(); - } - }; - - // 监听下载错误事件 - const handleDownloadError = (_, result) => { - if (result.filename === filename) { - isDownloading.value = false; - removeListeners(); - } - }; - - // 移除监听器函数 - const removeListeners = () => { - window.electron.ipcRenderer.removeListener('music-download-complete', handleDownloadComplete); - window.electron.ipcRenderer.removeListener('music-download-error', handleDownloadError); - }; - - // 添加事件监听器 - window.electron.ipcRenderer.once('music-download-complete', handleDownloadComplete); - window.electron.ipcRenderer.once('music-download-error', handleDownloadError); - - // 30秒后自动清理监听器(以防下载过程中出现未知错误) - setTimeout(removeListeners, 30000); + + // 简化的监听逻辑,基本通知由全局监听器处理 + setTimeout(() => { + isDownloading.value = false; + }, 2000); } catch (error: any) { console.error('Download error:', error); isDownloading.value = false; @@ -103,27 +224,17 @@ export const useDownload = () => { isDownloading.value = true; message.success(t('favorite.downloading')); - // 移除旧的监听器 - window.electron.ipcRenderer.removeAllListeners('music-download-complete'); - let successCount = 0; let failCount = 0; - - // 添加新的监听器 - window.electron.ipcRenderer.on('music-download-complete', (_, result) => { - if (result.success) { - successCount++; - } else { - failCount++; - } - - // 当所有下载完成时 - if (successCount + failCount === songs.length) { + const totalCount = songs.length; + + // 下载进度追踪 + const trackProgress = () => { + if (successCount + failCount === totalCount) { isDownloading.value = false; message.success(t('favorite.downloadSuccess')); - window.electron.ipcRenderer.removeAllListeners('music-download-complete'); } - }); + }; // 并行获取所有歌曲的下载链接 const downloadUrls = await Promise.all( @@ -133,6 +244,7 @@ export const useDownload = () => { return { song, ...data }; } catch (error) { console.error(`获取歌曲 ${song.name} 下载链接失败:`, error); + failCount++; return { song, url: null }; } }) @@ -142,21 +254,41 @@ export const useDownload = () => { downloadUrls.forEach(({ song, url, type }) => { if (!url) { failCount++; + trackProgress(); return; } + const songData = cloneDeep(song); + const filename = `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`; + + // 检查是否已在下载 + if (downloadManager.hasDownload(filename)) { + failCount++; + trackProgress(); + return; + } + + // 添加到活动下载集合 + downloadManager.addDownload(filename); + const songInfo = { ...songData, ar: songData.ar || songData.song?.artists, downloadTime: Date.now() }; + window.electron.ipcRenderer.send('download-music', { url, - filename: `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`, + filename, songInfo, type }); + + successCount++; }); + + // 所有下载开始后,检查进度 + trackProgress(); } catch (error) { console.error('下载失败:', error); isDownloading.value = false; diff --git a/src/renderer/views/download/DownloadPage.vue b/src/renderer/views/download/DownloadPage.vue index a239dfe..fe6dcc8 100644 --- a/src/renderer/views/download/DownloadPage.vue +++ b/src/renderer/views/download/DownloadPage.vue @@ -2,20 +2,26 @@