From d44be6022a760f021930feae9e1bfddf85dcca40 Mon Sep 17 00:00:00 2001 From: sqj Date: Sat, 11 Oct 2025 22:54:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BC=98=E5=8C=96=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E9=97=AE=E9=A2=98=EF=BC=8C=E6=9F=90=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=20=E6=AD=8C=E5=8D=95=E4=B8=8A=E9=99=90=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=8C=85=E4=BD=93=E7=A7=AF=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=AD=8C=E6=9B=B2=E4=B8=8B=E8=BD=BD=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 9 +- src/common/utils/quality.ts | 92 +++++ src/main/events/index.ts | 150 ++++++++ src/main/events/plugins.ts | 82 ++++ src/main/index.ts | 317 +--------------- src/main/utils/downloadSongs.ts | 355 +++++------------- src/main/utils/musicSdk/wy/songList.js | 2 +- src/renderer/components.d.ts | 1 + src/renderer/src/components/Play/FullPlay.vue | 140 ++++--- .../src/components/layout/HomeLayout.vue | 4 +- src/renderer/src/router/index.ts | 11 +- src/renderer/src/utils/audio/download.ts | 48 +-- src/renderer/src/views/home/index.vue | 7 - src/renderer/src/views/music/find.vue | 2 + src/renderer/src/views/music/list.vue | 114 ++++-- src/renderer/src/views/music/local.vue | 7 +- src/renderer/src/views/music/recent.vue | 4 +- src/renderer/src/views/music/search.vue | 1 + 18 files changed, 627 insertions(+), 719 deletions(-) create mode 100644 src/common/utils/quality.ts create mode 100644 src/main/events/index.ts create mode 100644 src/main/events/plugins.ts diff --git a/package.json b/package.json index b26a801..81bacc3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ceru-music", - "version": "1.4.2", + "version": "1.4.3", "description": "一款简洁优雅的音乐播放器", "main": "./out/main/index.js", "author": "sqj,wldss,star", @@ -47,7 +47,7 @@ "@pixi/filter-bulge-pinch": "^5.1.1", "@pixi/filter-color-matrix": "^7.4.3", "@pixi/sprite": "^7.4.3", - "@types/fluent-ffmpeg": "^2.1.27", + "@types/howler": "^2.2.12", "@types/needle": "^3.3.0", "NeteaseCloudMusicApi": "^4.27.0", "animate.css": "^4.1.1", @@ -57,8 +57,7 @@ "dompurify": "^3.2.6", "electron-log": "^5.4.3", "electron-updater": "^6.3.9", - "ffmpeg-static": "^5.2.0", - "fluent-ffmpeg": "^2.1.3", + "howler": "^2.2.4", "hpagent": "^1.2.0", "iconv-lite": "^0.7.0", "jss": "^10.10.0", @@ -69,7 +68,7 @@ "mitt": "^3.0.1", "needle": "^3.3.1", "node-fetch": "2", - "node-id3": "^0.2.9", + "node-taglib-sharp": "^6.0.1", "pinia": "^3.0.3", "pinia-plugin-persistedstate": "^4.5.0", "tdesign-icons-vue-next": "^0.4.1", diff --git a/src/common/utils/quality.ts b/src/common/utils/quality.ts new file mode 100644 index 0000000..697726f --- /dev/null +++ b/src/common/utils/quality.ts @@ -0,0 +1,92 @@ +export const QUALITY_ORDER = [ + 'master', + 'atmos_plus', + 'atmos', + 'hires', + 'flac24bit', + 'flac', + '320k', + '192k', + '128k' +] as const + +export type KnownQuality = (typeof QUALITY_ORDER)[number] +export type QualityInput = KnownQuality | string | { type: string; size?: string } + +const DISPLAY_NAME_MAP: Record = { + '128k': '标准', + '192k': '高品', + '320k': '超高', + flac: '无损', + flac24bit: '超高解析', + hires: '高清臻音', + atmos: '全景环绕', + atmos_plus: '全景增强', + master: '超清母带' +} + +/** + * 统一获取音质中文显示名称 + */ +export function getQualityDisplayName(quality: QualityInput | null | undefined): string { + if (!quality) return '' + const type = typeof quality === 'object' ? (quality as any).type : quality + return DISPLAY_NAME_MAP[type] || String(type || '') +} + +/** + * 比较两个音质优先级(返回负数表示 a 优于 b) + */ +export function compareQuality(aType: string, bType: string): number { + const ia = QUALITY_ORDER.indexOf(aType as KnownQuality) + const ib = QUALITY_ORDER.indexOf(bType as KnownQuality) + const va = ia === -1 ? QUALITY_ORDER.length : ia + const vb = ib === -1 ? QUALITY_ORDER.length : ib + return va - vb +} + +/** + * 规范化 types,兼容 string 与 {type,size} + */ +export function normalizeTypes( + types: Array | null | undefined +): string[] { + if (!types || !Array.isArray(types)) return [] + return types + .map((t) => (typeof t === 'object' ? (t as any).type : t)) + .filter((t): t is string => Boolean(t)) +} + +/** + * 获取数组中最高音质类型 + */ +export function getHighestQualityType( + types: Array | null | undefined +): string | null { + const arr = normalizeTypes(types) + if (!arr.length) return null + return arr.sort(compareQuality)[0] +} + +/** + * 构建并按优先级排序的 [{type, size}] 列表 + * 支持传入: + * - 数组:[{type,size}] + * - _types 映射:{ [type]: { size } } + */ +export function buildQualityFormats( + input: + | Array<{ type: string; size?: string }> + | Record + | null + | undefined +): Array<{ type: string; size?: string }> { + if (!input) return [] + let list: Array<{ type: string; size?: string }> + if (Array.isArray(input)) { + list = input.map((i) => ({ type: i.type, size: i.size })) + } else { + list = Object.keys(input).map((k) => ({ type: k, size: input[k]?.size })) + } + return list.sort((a, b) => compareQuality(a.type, b.type)) +} diff --git a/src/main/events/index.ts b/src/main/events/index.ts new file mode 100644 index 0000000..7744e42 --- /dev/null +++ b/src/main/events/index.ts @@ -0,0 +1,150 @@ +import InitPluginService from './plugins' +import '../services/musicSdk/index' +import aiEvents from '../events/ai' +import { app, powerSaveBlocker, Menu } from 'electron' +import path from 'node:path' +import { type BrowserWindow, Tray, ipcMain } from 'electron' +export default function InitEventServices(mainWindow: BrowserWindow) { + InitPluginService() + aiEvents(mainWindow) + basisEvent(mainWindow) +} + +function basisEvent(mainWindow: BrowserWindow) { + let psbId: number | null = null + let tray: Tray | null = null + let isQuitting = false + function createTray(): void { + // 创建系统托盘 + const trayIconPath = path.join(__dirname, '../../resources/logo.png') + tray = new Tray(trayIconPath) + + // 创建托盘菜单 + const contextMenu = Menu.buildFromTemplate([ + { + label: '显示窗口', + click: () => { + if (mainWindow) { + mainWindow.show() + mainWindow.focus() + } + } + }, + { + label: '播放/暂停', + click: () => { + // 这里可以添加播放控制逻辑 + console.log('music-control') + mainWindow?.webContents.send('music-control') + } + }, + { type: 'separator' }, + { + label: '退出', + click: () => { + isQuitting = true + app.quit() + } + } + ]) + + tray.setContextMenu(contextMenu) + tray.setToolTip('Ceru Music') + + // 单击托盘图标显示窗口 + tray.on('click', () => { + if (mainWindow) { + if (mainWindow.isVisible()) { + mainWindow.hide() + } else { + mainWindow.show() + mainWindow.focus() + } + } + }) + } + createTray() + // 应用退出前的清理 + app.on('before-quit', () => { + isQuitting = true + }) + + // 窗口控制 IPC 处理 + ipcMain.on('window-minimize', () => { + mainWindow.minimize() + }) + + ipcMain.on('window-maximize', () => { + if (mainWindow.isMaximized()) { + mainWindow.unmaximize() + } else { + mainWindow.maximize() + } + }) + + ipcMain.on('window-close', () => { + mainWindow.close() + }) + + // Mini 模式 IPC 处理 - 最小化到系统托盘 + ipcMain.on('window-mini-mode', (_, isMini) => { + if (mainWindow) { + if (isMini) { + // 进入 Mini 模式:隐藏窗口到系统托盘 + mainWindow.hide() + // 显示托盘通知(可选) + if (tray) { + tray.displayBalloon({ + title: '澜音 Music', + content: '已最小化到系统托盘啦,点击托盘图标可重新打开~' + }) + } + } else { + // 退出 Mini 模式:显示窗口 + mainWindow.show() + mainWindow.focus() + } + } + }) + + // 全屏模式 IPC 处理 + ipcMain.on('window-toggle-fullscreen', () => { + const isFullScreen = mainWindow.isFullScreen() + mainWindow.setFullScreen(!isFullScreen) + }) + // 阻止窗口关闭,改为隐藏到系统托盘 + mainWindow.on('close', (event) => { + if (!isQuitting) { + event.preventDefault() + mainWindow?.hide() + + // 显示托盘通知 + if (tray) { + tray.displayBalloon({ + title: 'Ceru Music', + content: '已最小化到系统托盘啦,点击托盘图标可重新打开~' + }) + } + } + }) + // 阻止系统息屏 IPC(开启/关闭) + ipcMain.handle('power-save-blocker:start', () => { + if (psbId == null) { + psbId = powerSaveBlocker.start('prevent-display-sleep') + } + return psbId + }) + + ipcMain.handle('power-save-blocker:stop', () => { + if (psbId != null && powerSaveBlocker.isStarted(psbId)) { + powerSaveBlocker.stop(psbId) + } + psbId = null + return true + }) + + // 获取应用版本号 + ipcMain.handle('get-app-version', () => { + return app.getVersion() + }) +} diff --git a/src/main/events/plugins.ts b/src/main/events/plugins.ts new file mode 100644 index 0000000..d0b2624 --- /dev/null +++ b/src/main/events/plugins.ts @@ -0,0 +1,82 @@ +import { ipcMain } from 'electron' +import pluginService from '../services/plugin' +function PluginEvent() { + + + ipcMain.handle('service-plugin-selectAndAddPlugin', async (_, type): Promise => { + try { + return await pluginService.selectAndAddPlugin(type) + } catch (error: any) { + console.error('Error selecting and adding plugin:', error) + return { error: error.message } + } + }) + + ipcMain.handle('service-plugin-downloadAndAddPlugin', async (_, url, type): Promise => { + try { + return await pluginService.downloadAndAddPlugin(url, type) + } catch (error: any) { + console.error('Error downloading and adding plugin:', error) + return { error: error.message } + } + }) + + ipcMain.handle('service-plugin-addPlugin', async (_, pluginCode, pluginName): Promise => { + try { + return await pluginService.addPlugin(pluginCode, pluginName) + } catch (error: any) { + console.error('Error adding plugin:', error) + return { error: error.message } + } + }) + + ipcMain.handle('service-plugin-getPluginById', async (_, id): Promise => { + try { + return pluginService.getPluginById(id) + } catch (error: any) { + console.error('Error getting plugin by id:', error) + return { error: error.message } + } + }) + + ipcMain.handle('service-plugin-loadAllPlugins', async (): Promise => { + try { + // 使用新的 getPluginsList 方法,但保持 API 兼容性 + return await pluginService.getPluginsList() + } catch (error: any) { + console.error('Error loading all plugins:', error) + return { error: error.message } + } + }) + + ipcMain.handle('service-plugin-getPluginLog', async (_, pluginId): Promise => { + try { + return await pluginService.getPluginLog(pluginId) + } catch (error: any) { + console.error('Error getting plugin log:', error) + return { error: error.message } + } + }) + + ipcMain.handle('service-plugin-uninstallPlugin', async (_, pluginId): Promise => { + try { + return await pluginService.uninstallPlugin(pluginId) + } catch (error: any) { + console.error('Error uninstalling plugin:', error) + return { error: error.message } + } + }) +} + +export default function InitPluginService() { + setTimeout(async () => { + // 初始化插件系统 + try { + await pluginService.initializePlugins() + PluginEvent() + console.log('插件系统初始化完成') + } catch (error) { + console.error('插件系统初始化失败:', error) + } + }, 1000) +} diff --git a/src/main/index.ts b/src/main/index.ts index 81c4c4f..25bb6c1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,12 +1,16 @@ -import { app, shell, BrowserWindow, ipcMain, Tray, Menu, screen, powerSaveBlocker } from 'electron' +import { app, shell, BrowserWindow, ipcMain, screen } from 'electron' import { configManager } from './services/ConfigManager' import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/logo.png?asset' import path from 'node:path' -import pluginService from './services/plugin' -import aiEvents from './events/ai' -import './services/musicSdk/index' +import InitEventServices from './events' + +import './events/musicCache' +import './events/songList' +import './events/directorySettings' +import './events/pluginNotice' + // 获取单实例锁 const gotTheLock = app.requestSingleInstanceLock() @@ -24,72 +28,9 @@ if (!gotTheLock) { } }) } - -// import wy from './utils/musicSdk/wy/index' -// import kg from './utils/musicSdk/kg/index' -// wy.hotSearch.getList().then((res) => { -// console.log(res) -// }) -// kg.hotSearch.getList().then((res) => { -// console.log(res) -// }) -let tray: Tray | null = null -let psbId: number | null = null let mainWindow: BrowserWindow | null = null -let isQuitting = false - -function createTray(): void { - // 创建系统托盘 - const trayIconPath = path.join(__dirname, '../../resources/logo.png') - tray = new Tray(trayIconPath) - - // 创建托盘菜单 - const contextMenu = Menu.buildFromTemplate([ - { - label: '显示窗口', - click: () => { - if (mainWindow) { - mainWindow.show() - mainWindow.focus() - } - } - }, - { - label: '播放/暂停', - click: () => { - // 这里可以添加播放控制逻辑 - console.log('music-control') - mainWindow?.webContents.send('music-control') - } - }, - { type: 'separator' }, - { - label: '退出', - click: () => { - isQuitting = true - app.quit() - } - } - ]) - - tray.setContextMenu(contextMenu) - tray.setToolTip('Ceru Music') - - // 双击托盘图标显示窗口 - tray.on('click', () => { - if (mainWindow) { - if (mainWindow.isVisible()) { - mainWindow.hide() - } else { - mainWindow.show() - mainWindow.focus() - } - } - }) -} function createWindow(): void { - // return // 获取保存的窗口位置和大小 const savedBounds = configManager.getWindowBounds() @@ -174,29 +115,11 @@ function createWindow(): void { mainWindow.on('ready-to-show', () => { mainWindow?.show() }) - - // 阻止窗口关闭,改为隐藏到系统托盘 - mainWindow.on('close', (event) => { - if (!isQuitting) { - event.preventDefault() - mainWindow?.hide() - - // 显示托盘通知 - if (tray) { - tray.displayBalloon({ - iconType: 'info', - title: 'Ceru Music', - content: '已最小化到系统托盘啦,点击托盘图标可重新打开~' - }) - } - } - }) - mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url).then() return { action: 'deny' } }) - + InitEventServices(mainWindow) // HMR for renderer base on electron-vite cli. // Load the remote URL for development or the local html file for production. if (is.dev && process.env['ELECTRON_RENDERER_URL']) { @@ -206,80 +129,6 @@ function createWindow(): void { } } -ipcMain.handle('service-plugin-selectAndAddPlugin', async (_, type): Promise => { - try { - return await pluginService.selectAndAddPlugin(type) - } catch (error: any) { - console.error('Error selecting and adding plugin:', error) - return { error: error.message } - } -}) - -ipcMain.handle('service-plugin-downloadAndAddPlugin', async (_, url, type): Promise => { - try { - return await pluginService.downloadAndAddPlugin(url, type) - } catch (error: any) { - console.error('Error downloading and adding plugin:', error) - return { error: error.message } - } -}) - -ipcMain.handle('service-plugin-addPlugin', async (_, pluginCode, pluginName): Promise => { - try { - return await pluginService.addPlugin(pluginCode, pluginName) - } catch (error: any) { - console.error('Error adding plugin:', error) - return { error: error.message } - } -}) - -ipcMain.handle('service-plugin-getPluginById', async (_, id): Promise => { - try { - return pluginService.getPluginById(id) - } catch (error: any) { - console.error('Error getting plugin by id:', error) - return { error: error.message } - } -}) - -ipcMain.handle('service-plugin-loadAllPlugins', async (): Promise => { - try { - // 使用新的 getPluginsList 方法,但保持 API 兼容性 - return await pluginService.getPluginsList() - } catch (error: any) { - console.error('Error loading all plugins:', error) - return { error: error.message } - } -}) - -ipcMain.handle('service-plugin-getPluginLog', async (_, pluginId): Promise => { - try { - return await pluginService.getPluginLog(pluginId) - } catch (error: any) { - console.error('Error getting plugin log:', error) - return { error: error.message } - } -}) - -ipcMain.handle('service-plugin-uninstallPlugin', async (_, pluginId): Promise => { - try { - return await pluginService.uninstallPlugin(pluginId) - } catch (error: any) { - console.error('Error uninstalling plugin:', error) - return { error: error.message } - } -}) - -// 获取应用版本号 -ipcMain.handle('get-app-version', () => { - return app.getVersion() -}) - -aiEvents(mainWindow) -import './events/musicCache' -import './events/songList' -import './events/directorySettings' -import './events/pluginNotice' import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate' // This method will be called when Electron has finished @@ -296,16 +145,6 @@ app.whenReady().then(() => { app.setName('澜音') } - setTimeout(async () => { - // 初始化插件系统 - try { - await pluginService.initializePlugins() - console.log('插件系统初始化完成') - } catch (error) { - console.error('插件系统初始化失败:', error) - } - }, 1000) - // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils @@ -313,79 +152,7 @@ app.whenReady().then(() => { optimizer.watchWindowShortcuts(window) }) - // 窗口控制 IPC 处理 - ipcMain.on('window-minimize', () => { - const window = BrowserWindow.getFocusedWindow() - if (window) { - window.minimize() - } - }) - - ipcMain.on('window-maximize', () => { - const window = BrowserWindow.getFocusedWindow() - if (window) { - if (window.isMaximized()) { - window.unmaximize() - } else { - window.maximize() - } - } - }) - - ipcMain.on('window-close', () => { - const window = BrowserWindow.getFocusedWindow() - if (window) { - window.close() - } - }) - - // Mini 模式 IPC 处理 - 最小化到系统托盘 - ipcMain.on('window-mini-mode', (_, isMini) => { - if (mainWindow) { - if (isMini) { - // 进入 Mini 模式:隐藏窗口到系统托盘 - mainWindow.hide() - // 显示托盘通知(可选) - if (tray) { - tray.displayBalloon({ - title: '澜音 Music', - content: '已最小化到系统托盘啦,点击托盘图标可重新打开~' - }) - } - } else { - // 退出 Mini 模式:显示窗口 - mainWindow.show() - mainWindow.focus() - } - } - }) - - // 全屏模式 IPC 处理 - ipcMain.on('window-toggle-fullscreen', () => { - if (mainWindow) { - const isFullScreen = mainWindow.isFullScreen() - mainWindow.setFullScreen(!isFullScreen) - } - }) - - // 阻止系统息屏 IPC(开启/关闭) - ipcMain.handle('power-save-blocker:start', () => { - if (psbId == null) { - psbId = powerSaveBlocker.start('prevent-display-sleep') - } - return psbId - }) - - ipcMain.handle('power-save-blocker:stop', () => { - if (psbId != null && powerSaveBlocker.isStarted(psbId)) { - powerSaveBlocker.stop(psbId) - } - psbId = null - return true - }) - createWindow() - createTray() // 注册自动更新事件 registerAutoUpdateEvents() @@ -415,67 +182,19 @@ app.on('window-all-closed', () => { // 在其他平台上,我们也保持应用运行,因为有系统托盘 }) -// 应用退出前的清理 -app.on('before-quit', () => { - isQuitting = true -}) - // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. let ping: NodeJS.Timeout function startPing() { - let interval = 3000 - + // 已迁移到 Howler,不再使用 DOM