diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 27da298..fbb90aa 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -40,6 +40,7 @@ module.exports = { 'vue/multi-word-component-names': 'off', 'no-nested-ternary': 'off', 'no-console': 'off', + 'no-await-in-loop': 'off', 'no-continue': 'off', 'no-restricted-syntax': 'off', 'no-return-assign': 'off', diff --git a/CHANGELOG.md b/CHANGELOG.md index dc759ca..3571744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # 更新日志 -## [v3.4.0] - 2025-01-11 +## [v3.5.0] - 2025-01-12 ### ✨ 新功能 -- 添加捐赠支持列表 -- 添加收藏列表 批量下载功能 -- 优化搜藏列表 可本地和线上收藏合并 如果收藏失败 会自动收藏到本地 +- 添加下载管理 进度显示 播放下载的音乐 +- 添加缓存管理 清理缓存 +- 优化下载格式问题 支持下载其他格式 ps:其实之前只是后缀名问题 \ No newline at end of file diff --git a/package.json b/package.json index 271cf39..6337456 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "AlgerMusicPlayer", - "version": "3.4.0", + "version": "3.5.0", "description": "Alger Music Player", "author": "Alger ", "main": "./out/main/index.js", diff --git a/src/main/modules/fileManager.ts b/src/main/modules/fileManager.ts index a2f91e3..a3c0e29 100644 --- a/src/main/modules/fileManager.ts +++ b/src/main/modules/fileManager.ts @@ -1,13 +1,53 @@ import axios from 'axios'; -import { app, dialog, ipcMain, shell } from 'electron'; +import { app, dialog, ipcMain, protocol, shell } from 'electron'; import Store from 'electron-store'; import * as fs from 'fs'; import * as path from 'path'; +const MAX_CONCURRENT_DOWNLOADS = 3; +const downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = []; +let activeDownloads = 0; + +// 创建一个store实例用于存储下载历史 +const downloadStore = new Store({ + name: 'downloads', + defaults: { + history: [] + } +}); + +// 创建一个store实例用于存储音频缓存 +const audioCacheStore = new Store({ + name: 'audioCache', + defaults: { + cache: {} + } +}); + /** * 初始化文件管理相关的IPC监听 */ export function initializeFileManager() { + // 注册本地文件协议 + protocol.registerFileProtocol('local', (request, callback) => { + try { + const decodedUrl = decodeURIComponent(request.url); + const filePath = decodedUrl.replace('local://', ''); + + // 检查文件是否存在 + if (!fs.existsSync(filePath)) { + console.error('File not found:', filePath); + callback({ error: -6 }); // net::ERR_FILE_NOT_FOUND + return; + } + + callback({ path: filePath }); + } catch (error) { + console.error('Error handling local protocol:', error); + callback({ error: -2 }); // net::FAILED + } + }); + // 通用的选择目录处理 ipcMain.handle('select-directory', async () => { const result = await dialog.showOpenDialog({ @@ -18,12 +58,194 @@ export function initializeFileManager() { }); // 通用的打开目录处理 - ipcMain.on('open-directory', (_, path) => { - shell.openPath(path); + ipcMain.on('open-directory', (_, filePath) => { + try { + if (fs.statSync(filePath).isDirectory()) { + shell.openPath(filePath); + } else { + shell.showItemInFolder(filePath); + } + } catch (error) { + console.error('Error opening path:', error); + } }); // 下载音乐处理 - ipcMain.on('download-music', downloadMusic); + ipcMain.on('download-music', handleDownloadRequest); + + // 检查文件是否已下载 + ipcMain.handle('check-music-downloaded', (_, filename: string) => { + const store = new Store(); + const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads'); + const filePath = path.join(downloadPath, `${filename}.mp3`); + return fs.existsSync(filePath); + }); + + // 删除已下载的音乐 + ipcMain.handle('delete-downloaded-music', async (_, filePath: string) => { + try { + if (fs.existsSync(filePath)) { + // 先删除文件 + try { + await fs.promises.unlink(filePath); + } catch (error) { + console.error('Error deleting file:', error); + } + + // 删除对应的歌曲信息 + const store = new Store(); + const songInfos = store.get('downloadedSongs', {}) as Record; + delete songInfos[filePath]; + store.set('downloadedSongs', songInfos); + + return true; + } + return false; + } catch (error) { + console.error('Error deleting file:', error); + return false; + } + }); + + // 获取已下载音乐列表 + ipcMain.handle('get-downloaded-music', () => { + try { + const store = new Store(); + const songInfos = store.get('downloadedSongs', {}) as Record; + + // 过滤出实际存在的文件 + const validSongs = Object.entries(songInfos) + .filter(([path]) => fs.existsSync(path)) + .map(([_, info]) => info) + .sort((a, b) => (b.downloadTime || 0) - (a.downloadTime || 0)); + + // 更新存储,移除不存在的文件记录 + const newSongInfos = validSongs.reduce((acc, song) => { + acc[song.path] = song; + return acc; + }, {}); + store.set('downloadedSongs', newSongInfos); + + return validSongs; + } catch (error) { + console.error('Error getting downloaded music:', error); + return []; + } + }); + + // 检查歌曲是否已下载并返回本地路径 + ipcMain.handle('check-song-downloaded', (_, songId: number) => { + const store = new Store(); + const songInfos = store.get('downloadedSongs', {}) as Record; + + // 通过ID查找已下载的歌曲 + for (const [path, info] of Object.entries(songInfos)) { + if (info.id === songId && fs.existsSync(path)) { + return { + isDownloaded: true, + localPath: `local://${path}`, + songInfo: info + }; + } + } + + return { + isDownloaded: false, + localPath: '', + songInfo: null + }; + }); + + // 添加清除下载历史的处理函数 + ipcMain.on('clear-downloads-history', () => { + downloadStore.set('history', []); + }); + + // 添加清除音频缓存的处理函数 + ipcMain.on('clear-audio-cache', () => { + audioCacheStore.set('cache', {}); + // 清除临时音频文件目录 + const tempDir = path.join(app.getPath('userData'), 'AudioCache'); + if (fs.existsSync(tempDir)) { + try { + fs.readdirSync(tempDir).forEach((file) => { + const filePath = path.join(tempDir, file); + if (file.endsWith('.mp3') || file.endsWith('.m4a')) { + fs.unlinkSync(filePath); + } + }); + } catch (error) { + console.error('清除音频缓存文件失败:', error); + } + } + }); +} + +/** + * 处理下载请求 + */ +function handleDownloadRequest( + event: Electron.IpcMainEvent, + { + url, + filename, + songInfo, + type + }: { url: string; filename: string; songInfo?: any; type?: string } +) { + // 检查是否已经在队列中或正在下载 + if (downloadQueue.some((item) => item.filename === filename)) { + event.reply('music-download-error', { + filename, + error: '该歌曲已在下载队列中' + }); + return; + } + + // 检查是否已下载 + const store = new Store(); + const songInfos = store.get('downloadedSongs', {}) as Record; + + // 检查是否已下载(通过ID) + const isDownloaded = + songInfo?.id && Object.values(songInfos).some((info: any) => info.id === songInfo.id); + + if (isDownloaded) { + event.reply('music-download-error', { + filename, + error: '该歌曲已下载' + }); + return; + } + + // 添加到下载队列 + downloadQueue.push({ url, filename, songInfo, type }); + event.reply('music-download-queued', { + filename, + songInfo + }); + + // 尝试开始下载 + processDownloadQueue(event); +} + +/** + * 处理下载队列 + */ +async function processDownloadQueue(event: Electron.IpcMainEvent) { + if (activeDownloads >= MAX_CONCURRENT_DOWNLOADS || downloadQueue.length === 0) { + return; + } + + const { url, filename, songInfo, type } = downloadQueue.shift()!; + activeDownloads++; + + try { + await downloadMusic(event, { url, filename, songInfo, type }); + } finally { + activeDownloads--; + processDownloadQueue(event); + } } /** @@ -31,17 +253,26 @@ export function initializeFileManager() { */ async function downloadMusic( event: Electron.IpcMainEvent, - { url, filename }: { url: string; filename: string } + { + url, + filename, + songInfo, + type = 'mp3' + }: { url: string; filename: string; songInfo: any; type?: string } ) { + let finalFilePath = ''; + let writer: fs.WriteStream | null = null; + try { const store = new Store(); const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads'); - // 直接使用配置的下载路径 - const filePath = path.join(downloadPath, `${filename}.mp3`); + // 从URL中获取文件扩展名,如果没有则使用传入的type或默认mp3 + const urlExt = type ? `.${type}` : '.mp3'; + const filePath = path.join(downloadPath, `${filename}${urlExt}`); // 检查文件是否已存在,如果存在则添加序号 - let finalFilePath = filePath; + finalFilePath = filePath; let counter = 1; while (fs.existsSync(finalFilePath)) { const ext = path.extname(filePath); @@ -50,23 +281,115 @@ async function downloadMusic( counter++; } + // 先获取文件大小 + const headResponse = await axios.head(url); + const totalSize = parseInt(headResponse.headers['content-length'] || '0', 10); + + // 开始下载 const response = await axios({ url, method: 'GET', - responseType: 'stream' + responseType: 'stream', + timeout: 30000 // 30秒超时 }); - const writer = fs.createWriteStream(finalFilePath); - response.data.pipe(writer); + writer = fs.createWriteStream(finalFilePath); + let downloadedSize = 0; - writer.on('finish', () => { - event.reply('music-download-complete', { success: true, path: finalFilePath }); + // 使用 data 事件来跟踪下载进度 + response.data.on('data', (chunk: Buffer) => { + downloadedSize += chunk.length; + const progress = Math.round((downloadedSize / totalSize) * 100); + event.reply('music-download-progress', { + filename, + progress, + loaded: downloadedSize, + total: totalSize, + path: finalFilePath, + status: progress === 100 ? 'completed' : 'downloading', + songInfo: songInfo || { + name: filename, + ar: [{ name: '本地音乐' }], + picUrl: '/images/default_cover.png' + } + }); }); - writer.on('error', (err) => { - event.reply('music-download-complete', { success: false, error: err.message }); + // 等待下载完成 + await new Promise((resolve, reject) => { + writer!.on('finish', resolve); + writer!.on('error', reject); + response.data.pipe(writer!); }); + + // 验证文件是否完整下载 + const stats = fs.statSync(finalFilePath); + if (stats.size !== totalSize) { + throw new Error('文件下载不完整'); + } + + // 保存下载信息 + try { + const songInfos = store.get('downloadedSongs', {}) as Record; + const defaultInfo = { + name: filename, + ar: [{ name: '本地音乐' }], + picUrl: '/images/default_cover.png' + }; + + const newSongInfo = { + id: songInfo?.id || 0, + name: songInfo?.name || filename, + filename, + picUrl: songInfo?.picUrl || defaultInfo.picUrl, + ar: songInfo?.ar || defaultInfo.ar, + size: totalSize, + path: finalFilePath, + downloadTime: Date.now(), + al: songInfo?.al || { picUrl: songInfo?.picUrl || defaultInfo.picUrl }, + type: type || 'mp3' + }; + + // 保存到下载记录 + songInfos[finalFilePath] = newSongInfo; + store.set('downloadedSongs', songInfos); + + // 添加到下载历史 + const history = downloadStore.get('history', []) as any[]; + history.unshift(newSongInfo); + downloadStore.set('history', history); + + // 发送下载完成事件 + event.reply('music-download-complete', { + success: true, + path: finalFilePath, + filename, + size: totalSize, + songInfo: newSongInfo + }); + } catch (error) { + console.error('Error saving download info:', error); + throw new Error('保存下载信息失败'); + } } catch (error: any) { - event.reply('music-download-complete', { success: false, error: error.message }); + console.error('Download error:', error); + + // 清理未完成的下载 + if (writer) { + writer.end(); + } + if (finalFilePath && fs.existsSync(finalFilePath)) { + try { + fs.unlinkSync(finalFilePath); + } catch (e) { + console.error('Failed to delete incomplete download:', e); + } + } + + event.reply('music-download-complete', { + success: false, + error: error.message || '下载失败', + filename + }); } } diff --git a/src/renderer/api/music.ts b/src/renderer/api/music.ts index 444aaa1..43e3d50 100644 --- a/src/renderer/api/music.ts +++ b/src/renderer/api/music.ts @@ -19,7 +19,7 @@ export const getMusicUrl = async (id: number) => { }); if (res.data.data.url) { - return { data: { data: [{ url: res.data.data.url }] } }; + return { data: { data: [{ ...res.data.data }] } }; } return await request.get('/song/url/v1', { diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts index 12c6df2..e0b2b1b 100644 --- a/src/renderer/components.d.ts +++ b/src/renderer/components.d.ts @@ -8,12 +8,16 @@ export {} declare module 'vue' { export interface GlobalComponents { NAvatar: typeof import('naive-ui')['NAvatar'] + NBadge: typeof import('naive-ui')['NBadge'] NButton: typeof import('naive-ui')['NButton'] NButtonGroup: typeof import('naive-ui')['NButtonGroup'] + NCard: typeof import('naive-ui')['NCard'] NCheckbox: typeof import('naive-ui')['NCheckbox'] + NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDrawer: typeof import('naive-ui')['NDrawer'] + NDrawerContent: typeof import('naive-ui')['NDrawerContent'] NDropdown: typeof import('naive-ui')['NDropdown'] NEllipsis: typeof import('naive-ui')['NEllipsis'] NEmpty: typeof import('naive-ui')['NEmpty'] @@ -26,13 +30,16 @@ declare module 'vue' { NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NModal: typeof import('naive-ui')['NModal'] NPopover: typeof import('naive-ui')['NPopover'] + NProgress: typeof import('naive-ui')['NProgress'] NScrollbar: typeof import('naive-ui')['NScrollbar'] NSelect: typeof import('naive-ui')['NSelect'] NSlider: typeof import('naive-ui')['NSlider'] + NSpace: typeof import('naive-ui')['NSpace'] NSpin: typeof import('naive-ui')['NSpin'] NSwitch: typeof import('naive-ui')['NSwitch'] + NTabPane: typeof import('naive-ui')['NTabPane'] + NTabs: typeof import('naive-ui')['NTabs'] NTag: typeof import('naive-ui')['NTag'] - NText: typeof import('naive-ui')['NText'] NTooltip: typeof import('naive-ui')['NTooltip'] NVirtualList: typeof import('naive-ui')['NVirtualList'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/src/renderer/components/MvPlayer.vue b/src/renderer/components/MvPlayer.vue index 425c21c..06012d8 100644 --- a/src/renderer/components/MvPlayer.vue +++ b/src/renderer/components/MvPlayer.vue @@ -315,7 +315,6 @@ onUnmounted(() => { if (cursorTimer) { clearTimeout(cursorTimer); } - unlockScreenOrientation(); }); // 监听 currentMv 的变化 @@ -416,27 +415,6 @@ const checkFullscreenAPI = () => { }; }; -// 添加横屏锁定功能 -const lockScreenOrientation = async () => { - try { - if ('orientation' in screen) { - await (screen as any).orientation.lock('landscape'); - } - } catch (error) { - console.warn('无法锁定屏幕方向:', error); - } -}; - -const unlockScreenOrientation = () => { - try { - if ('orientation' in screen) { - (screen as any).orientation.unlock(); - } - } catch (error) { - console.warn('无法解锁屏幕方向:', error); - } -}; - // 修改切换全屏状态的方法 const toggleFullscreen = async () => { const api = checkFullscreenAPI(); @@ -450,17 +428,9 @@ const toggleFullscreen = async () => { if (!api.fullscreenElement) { await videoContainerRef.value?.requestFullscreen(); isFullscreen.value = true; - // 在移动端进入全屏时锁定横屏 - if (window.innerWidth <= 768) { - await lockScreenOrientation(); - } } else { await document.exitFullscreen(); isFullscreen.value = false; - // 退出全屏时解锁屏幕方向 - if (window.innerWidth <= 768) { - unlockScreenOrientation(); - } } } catch (error) { console.error('切换全屏失败:', error); diff --git a/src/renderer/components/common/DownloadDrawer.vue b/src/renderer/components/common/DownloadDrawer.vue new file mode 100644 index 0000000..3d91206 --- /dev/null +++ b/src/renderer/components/common/DownloadDrawer.vue @@ -0,0 +1,590 @@ + + + + + diff --git a/src/renderer/components/common/SongItem.vue b/src/renderer/components/common/SongItem.vue index 4eea640..9672931 100644 --- a/src/renderer/components/common/SongItem.vue +++ b/src/renderer/components/common/SongItem.vue @@ -158,43 +158,61 @@ const downloadMusic = async () => { try { isDownloading.value = true; - const loadingMessage = message.loading('正在下载中...', { duration: 0 }); - const url = await getSongUrl(props.item.id, cloneDeep(props.item)); - if (!url) { - loadingMessage.destroy(); - message.error('获取音乐下载地址失败'); - isDownloading.value = false; - return; + const data = (await getSongUrl(props.item.id, cloneDeep(props.item), true)) as any; + if (!data || !data.url) { + throw new Error('获取音乐下载地址失败'); } - // 先移除可能存在的旧监听器 - window.electron.ipcRenderer.removeAllListeners('music-download-complete'); + // 构建文件名 + const artistNames = (props.item.ar || props.item.song?.artists)?.map((a) => a.name).join(','); + const filename = `${props.item.name} - ${artistNames}`; // 发送下载请求 window.electron.ipcRenderer.send('download-music', { - url, - filename: `${props.item.name} - ${(props.item.ar || props.item.song?.artists)?.map((a) => a.name).join(',')}` - }); - - // 添加新的一次性监听器 - window.electron.ipcRenderer.once('music-download-complete', (_, result) => { - isDownloading.value = false; - loadingMessage.destroy(); - - if (result.success) { - message.success('下载成功'); - } else if (result.canceled) { - // 用户取消了保存 - message.info('已取消下载'); - } else { - message.error(`下载失败: ${result.error}`); + url: data.url, + type: data.type, + filename, + songInfo: { + ...cloneDeep(props.item), + downloadTime: Date.now() } }); - } catch (error) { + + message.success('已加入下载队列'); + + // 监听下载完成事件 + 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); + } catch (error: any) { + console.error('Download error:', error); isDownloading.value = false; - message.destroyAll(); - message.error('下载失败'); + message.error(error.message || '下载失败'); } }; diff --git a/src/renderer/hooks/MusicListHook.ts b/src/renderer/hooks/MusicListHook.ts index e7925dd..104beb5 100644 --- a/src/renderer/hooks/MusicListHook.ts +++ b/src/renderer/hooks/MusicListHook.ts @@ -11,24 +11,34 @@ import { getImageLinearBackground } from '@/utils/linearColor'; const musicHistory = useMusicHistory(); // 获取歌曲url -export const getSongUrl = async (id: number, songData: any) => { +export const getSongUrl = async (id: number, songData: any, isDownloaded: boolean = false) => { const { data } = await getMusicUrl(id); + console.log('data', data); let url = ''; + let songDetail = null; try { if (data.data[0].freeTrialInfo || !data.data[0].url) { const res = await getParsingMusicUrl(id, songData); + console.log('res', res); 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 = await getSongUrl(playMusic.id, cloneDeep(playMusic)); + const playMusicUrl = + playMusic.playMusicUrl || (await getSongUrl(playMusic.id, cloneDeep(playMusic))); const { backgroundColor, primaryColor } = playMusic.backgroundColor && playMusic.primaryColor ? playMusic @@ -42,6 +52,7 @@ const getSongDetail = async (playMusic: SongResult) => { export const useMusicListHook = () => { const handlePlayMusic = async (state: any, playMusic: SongResult) => { const updatedPlayMusic = await getSongDetail(playMusic); + console.log('updatedPlayMusic', updatedPlayMusic); state.playMusic = updatedPlayMusic; state.playMusicUrl = updatedPlayMusic.playMusicUrl; state.play = true; diff --git a/src/renderer/layout/AppLayout.vue b/src/renderer/layout/AppLayout.vue index 08bf6d2..4382f5a 100644 --- a/src/renderer/layout/AppLayout.vue +++ b/src/renderer/layout/AppLayout.vue @@ -26,6 +26,8 @@ + + @@ -37,6 +39,7 @@ import { computed, defineAsyncComponent, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { useStore } from 'vuex'; +import DownloadDrawer from '@/components/common/DownloadDrawer.vue'; import InstallAppModal from '@/components/common/InstallAppModal.vue'; import PlayBottom from '@/components/common/PlayBottom.vue'; import UpdateModal from '@/components/common/UpdateModal.vue'; diff --git a/src/renderer/views/favorite/index.vue b/src/renderer/views/favorite/index.vue index 5db4206..50407cc 100644 --- a/src/renderer/views/favorite/index.vue +++ b/src/renderer/views/favorite/index.vue @@ -5,7 +5,7 @@

我的收藏

共 {{ favoriteList.length }} 首
-
+