[开源项目] 一款功能强大的超高音质音乐播放器
文章目录
- 项目简介
- 预览地址
- 功能实现
- 音源解析
- 项目启动
- 项目打包
- 关注我的CDDN博客
主要功能如下
-
🎵 音乐推荐
-
🔐 网易云账号登录与同步
-
📝 功能
- 播放历史记录
- 歌曲收藏管理
- 歌单 MV 排行榜 每日推荐
- 自定义快捷键配置(全局或应用内)
-
🎨 界面与交互
- 沉浸式歌词显示(点击左下角封面进入)
- 独立桌面歌词窗口
- 明暗主题切换
- 迷你模式
- 状态栏控制
- 多语言支持
-
🎼 音乐功能
- 支持歌单、MV、专辑等完整音乐服务
- 灰色音乐资源解析(基于 @unblockneteasemusic/server)
- 音乐单独解析
- EQ均衡器
- 定时播放 远程控制播放 倍速播放
- 高品质音乐
- 音乐文件下载(支持右键下载和批量下载, 附带歌词封面等信息)
- 搜索 MV 音乐 专辑 歌单 bilibili
- 音乐单独选择音源解析
-
🚀 技术特性
- 本地化服务,无需依赖在线API (基于 netease-cloud-music-api)
- 全平台适配(Desktop & Web & Mobile Web & Android<测试> & ios<后续>)
项目简介
一个第三方音乐播放器、本地服务、桌面歌词、音乐下载、最高音质
预览地址
http://music.alger.fun/
功能实现
/*** 快捷键配置*/
export interface ShortcutConfig {/** 快捷键字符串 */key: string;/** 是否启用 */enabled: boolean;/** 作用范围: global(全局) 或 app(仅应用内) */scope: 'global' | 'app';
}/*** 快捷键配置集合*/
export interface ShortcutsConfig {[key: string]: ShortcutConfig;
}
import { BrowserWindow, IpcMain, screen } from 'electron';
import Store from 'electron-store';
import path, { join } from 'path';const store = new Store();
let lyricWindow: BrowserWindow | null = null;// 跟踪拖动状态
let isDragging = false;// 添加窗口大小变化防护
let originalSize = { width: 0, height: 0 };const createWin = () => {console.log('Creating lyric window');// 获取保存的窗口位置const windowBounds =(store.get('lyricWindowBounds') as {x?: number;y?: number;width?: number;height?: number;displayId?: number;}) || {};const { x, y, width, height, displayId } = windowBounds;// 获取所有屏幕的信息const displays = screen.getAllDisplays();let isValidPosition = false;let targetDisplay = displays[0]; // 默认使用主显示器// 如果有显示器ID,尝试按ID匹配if (displayId) {const matchedDisplay = displays.find((d) => d.id === displayId);if (matchedDisplay) {targetDisplay = matchedDisplay;console.log('Found matching display by ID:', displayId);}}// 验证位置是否在任何显示器的范围内if (x !== undefined && y !== undefined) {for (const display of displays) {const { bounds } = display;if (x >= bounds.x - 50 && // 允许一点偏移,避免卡在边缘x < bounds.x + bounds.width + 50 &&y >= bounds.y - 50 &&y < bounds.y + bounds.height + 50) {isValidPosition = true;targetDisplay = display;break;}}}// 确保宽高合理const defaultWidth = 800;const defaultHeight = 200;const maxWidth = 1600; // 设置最大宽度限制const maxHeight = 800; // 设置最大高度限制const validWidth = width && width > 0 && width <= maxWidth ? width : defaultWidth;const validHeight = height && height > 0 && height <= maxHeight ? height : defaultHeight;// 确定窗口位置let windowX = isValidPosition ? x : undefined;let windowY = isValidPosition ? y : undefined;// 如果位置无效,默认在当前显示器中居中if (windowX === undefined || windowY === undefined) {windowX = targetDisplay.bounds.x + (targetDisplay.bounds.width - validWidth) / 2;windowY = targetDisplay.bounds.y + (targetDisplay.bounds.height - validHeight) / 2;}lyricWindow = new BrowserWindow({width: validWidth,height: validHeight,x: windowX,y: windowY,frame: false,show: false,transparent: true,opacity: 1,hasShadow: false,alwaysOnTop: true,resizable: true,roundedCorners: false,titleBarStyle: 'hidden',titleBarOverlay: false,// 添加跨屏幕支持选项webPreferences: {preload: join(__dirname, '../preload/index.js'),sandbox: false,contextIsolation: true},backgroundColor: '#00000000'});// 监听窗口关闭事件lyricWindow.on('closed', () => {if (lyricWindow) {lyricWindow.destroy();lyricWindow = null;}});// 监听窗口大小变化事件,保存新的尺寸lyricWindow.on('resize', () => {// 如果正在拖动,忽略大小调整事件if (isDragging) return;if (lyricWindow && !lyricWindow.isDestroyed()) {const [width, height] = lyricWindow.getSize();const [x, y] = lyricWindow.getPosition();// 保存窗口位置和大小store.set('lyricWindowBounds', { x, y, width, height });}});lyricWindow.on('blur', () => lyricWindow && lyricWindow.setMaximizable(false))return lyricWindow;
};export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => {const showLyricWindow = () => {if (lyricWindow && !lyricWindow.isDestroyed()) {if (lyricWindow.isMinimized()) {lyricWindow.restore();}lyricWindow.focus();lyricWindow.show();return true;}return false;};ipcMain.on('open-lyric', () => {console.log('Received open-lyric request');if (showLyricWindow()) {return;}console.log('Creating new lyric window');const win = createWin();if (!win) {console.error('Failed to create lyric window');return;}if (process.env.NODE_ENV === 'development') {win.webContents.openDevTools({ mode: 'detach' });win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/#/lyric`);} else {const distPath = path.resolve(__dirname, '../renderer');win.loadURL(`file://${distPath}/index.html#/lyric`);}win.setMinimumSize(600, 200);win.setSkipTaskbar(true);win.once('ready-to-show', () => {console.log('Lyric window ready to show');win.show();});});ipcMain.on('send-lyric', (_, data) => {if (lyricWindow && !lyricWindow.isDestroyed()) {try {lyricWindow.webContents.send('receive-lyric', data);} catch (error) {console.error('Error processing lyric data:', error);}}});ipcMain.on('top-lyric', (_, data) => {if (lyricWindow && !lyricWindow.isDestroyed()) {lyricWindow.setAlwaysOnTop(data);}});ipcMain.on('close-lyric', () => {if (lyricWindow && !lyricWindow.isDestroyed()) {lyricWindow.webContents.send('lyric-window-close');mainWin.webContents.send('lyric-control-back', 'close');mainWin.webContents.send('lyric-window-closed');lyricWindow.destroy();lyricWindow = null;}});// 处理鼠标事件ipcMain.on('mouseenter-lyric', () => {if (lyricWindow && !lyricWindow.isDestroyed()) {lyricWindow.setIgnoreMouseEvents(true);}});ipcMain.on('mouseleave-lyric', () => {if (lyricWindow && !lyricWindow.isDestroyed()) {lyricWindow.setIgnoreMouseEvents(false);}});// 开始拖动时设置标志ipcMain.on('lyric-drag-start', () => {isDragging = true;if (lyricWindow && !lyricWindow.isDestroyed()) {// 记录原始窗口大小const [width, height] = lyricWindow.getSize();originalSize = { width, height };// 在拖动时暂时禁用大小调整lyricWindow.setResizable(false);}});// 结束拖动时清除标志ipcMain.on('lyric-drag-end', () => {isDragging = false;if (lyricWindow && !lyricWindow.isDestroyed()) {// 确保窗口大小恢复原样lyricWindow.setSize(originalSize.width, originalSize.height);// 拖动结束后恢复可调整大小lyricWindow.setResizable(true);}});// 处理拖动移动ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {if (!lyricWindow || lyricWindow.isDestroyed() || !isDragging) return;const [currentX, currentY] = lyricWindow.getPosition();// 使用记录的原始大小,而不是当前大小const windowWidth = originalSize.width;const windowHeight = originalSize.height;// 计算新位置const newX = currentX + deltaX;const newY = currentY + deltaY;try {// 获取当前鼠标所在的显示器const mousePoint = screen.getCursorScreenPoint();const currentDisplay = screen.getDisplayNearestPoint(mousePoint);// 拖动期间使用setBounds确保大小不变,使用false避免动画卡顿lyricWindow.setBounds({x: newX,y: newY,width: windowWidth,height: windowHeight},false);// 更新存储的位置const windowBounds = {x: newX,y: newY,width: windowWidth,height: windowHeight,displayId: currentDisplay.id // 记录当前显示器ID,有助于多屏幕处理};store.set('lyricWindowBounds', windowBounds);} catch (error) {console.error('Error during window drag:', error);// 出错时尝试使用更简单的方法lyricWindow.setPosition(newX, newY);}});// 添加鼠标穿透事件处理ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => {if (!lyricWindow || lyricWindow.isDestroyed()) return;lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });});// 添加播放控制处理ipcMain.on('control-back', (_, command) => {console.log('command', command);if (mainWin && !mainWin.isDestroyed()) {console.log('Sending control-back command:', command);mainWin.webContents.send('lyric-control-back', command);}});
};
音源解析
import { ipcMain } from 'electron';
import Store from 'electron-store';
import fs from 'fs';
import server from 'netease-cloud-music-api-alger/server';
import os from 'os';
import path from 'path';import { unblockMusic, type Platform } from './unblockMusic';const store = new Store();
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
}// 设置音乐解析的处理程序
ipcMain.handle('unblock-music', async (_event, id, songData, enabledSources) => {try {const result = await unblockMusic(id, songData, 1, enabledSources as Platform[]);return result;} catch (error) {console.error('音乐解析失败:', error);return { error: (error as Error).message || '未知错误' };}
});async function startMusicApi(): Promise<void> {console.log('MUSIC API STARTED');const port = (store.get('set') as any).musicApiPort || 30488;await server.serveNcmApi({port});
}export { startMusicApi };
项目启动
npm install
npm run dev
项目打包
# web
npm run build
# win
npm run build:win
# mac
npm run build:mac
# linux
npm run build:linux
💯 👉【我的更新汇总】
👉项目地址:
关注我的CDDN博客
更多资源可以查看我的CSDN博客