Repository Wiki
ldx123000/Hydrogen-Music

项目概览

Hydrogen Music 是一个基于 Electron、Vue 3 与 Vite 的第三方桌面音乐播放器复活版,面向网易云音乐能力、桌面播放体验以及 Windows、macOS、Linux 多平台集成。

Purpose and Scope

本页从项目级别说明 Hydrogen Music 的产品定位、运行时边界、主要功能域、桌面端启动架构和开发/发行入口。重点覆盖仓库根目录已经明确的 Electron 主进程、Vite/Vue 渲染入口、本地网易云 API 服务、持久化存储和跨平台桌面集成。

本页不展开某一个具体页面、播放器状态管理、歌词解析、下载器实现或 Android 子工程的内部代码;这些属于更细粒度的功能专题。README 明确 Android 工程位于 apps/android/,因此这里只把它作为独立发行目标提及。对于具体功能实现,应继续查看相应专题页面和 src/、apps/android/ 下的实现。

Overview

项目的核心目标是恢复一个“可用、稳定、完整”的桌面音乐客户端。README 将能力分为账号与服务、播放、曲库与搜索、私人漫游、歌词与评论、下载/本地音乐/云盘、视频/电台/扩展音源以及桌面端集成等领域。

从运行时角度看,项目由两部分组成:

  • Electron 主进程:入口由 package.json 的 main 指向 background.js。主进程负责应用生命周期、窗口、单实例约束、IPC、快捷键清理、更新标记、Cookie 清理以及启动本地网易云 API。
  • Vite/Vue 渲染端:根 index.html 将页面挂载到 #app,并以模块方式加载 /src/main.js。开发时由 Vite 提供前端资源,Electron 负责承载桌面窗口。

项目同时把本地服务和桌面集成作为一等能力:README 说明应用启动后会自动拉起增强版网易云 API,本地默认端口为 36530;background.js 则在创建窗口后调用 startNeteaseMusicApi(),并通过 Promise 状态让渲染进程等待 API 就绪。

Architecture

Loading diagram...

该图中的连接均对应仓库中的启动关系:background.js 是 Electron 入口;它创建窗口并加载构建后的 dist/index.html,而 index.html 再加载 src/main.js。主进程在应用 ready 后启动窗口和本地网易云 API,并依据平台条件加载 Linux MPRIS 模块。electron-store 用于设置和窗口状态存储,这也是桌面运行时与渲染端用户偏好之间的持久化边界。

Source: background.js

Source: index.html

项目分层与边界

产品能力层

README 将项目描述为网易云音乐风格的桌面播放器,并列出登录、曲库访问、播放解析、下载、云盘、私人漫游、评论、桌面歌词、音乐视频、本地音乐和塞壬唱片等能力。这些能力共同构成面向用户的音乐产品层,但它们并不改变桌面壳的基本职责:提供窗口、生命周期、系统集成和与本地服务通信的宿主环境。

播放能力包含多种音质档位、歌单/专辑/歌手/每日推荐等来源,以及顺序、循环、单曲循环和随机播放模式。README 还指出播放队列支持持久化和断点恢复,并可通过预缓冲下一首降低切歌空隙。这些是播放器领域的行为约束;项目概览只记录其产品承诺,不把未在当前已读文件中出现的具体实现类或状态字段当作已知事实。

桌面壳层

桌面层在 background.js 中显式处理以下职责:

  1. 启动前设置 Chromium 解码和 GPU 相关开关。
  2. 通过 app.requestSingleInstanceLock() 保证单实例;第二次启动时恢复、显示并聚焦已有窗口。
  3. 在 app.whenReady() 中注册未处理异常日志,创建主窗口并异步启动本地 API。
  4. 注册 ncm-api-ready-state 和 ncm-api-cookie-clear 两个 IPC handler,分别提供 API 就绪状态和 Cookie 清理能力。
  5. 在应用退出前注销全局快捷键,并在非 macOS 平台所有窗口关闭时退出应用。

这种分工使渲染端不需要直接承担 Electron 生命周期管理,也使本地 API 启动失败可以被转换为明确的 ready/error 状态,而不是让渲染端无限猜测服务是否已可用。

启动与初始化顺序

应用启动有两个互相配合的初始化链路:桌面窗口链路和本地 API 链路。窗口先被创建,API 随后异步启动;API 的结果通过 resolveNcmApiReady() 广播给已等待的调用方。

Loading diagram...

实现上的关键点是 waitForNcmApiReady():若 API 已经完成初始化,调用立即返回缓存 payload;否则把 Promise resolver 放入 ncmApiReadyWaiters,待 API 完成后逐个唤醒。这避免了启动竞态,尤其适用于渲染端加载速度快于本地服务启动速度的场景。

Source: background.js

Source: background.js

开发、构建与发行入口

package.json 把开发、构建和发行拆成独立脚本:Vite 负责 dev、build、preview;Electron 由 start 脚本通过 nodemon 启动;dist 先构建前端,再执行 scripts/build.js 完成发行打包。README 要求 Node.js 20.19.0+ 或 22.12.0+,并说明开发时需要同时启动 Vite 服务和 Electron 客户端。

正式发行覆盖 Windows 的 NSIS/Portable/Zip、macOS 的 DMG 以及 Linux 的 AppImage/Deb/RPM。Android 则通过 apps/android/ 的独立脚本链路构建 APK/AAB,和桌面 Electron 运行时分开。

基础开发命令

bash
npm ci npm run dev npm start

Sources:

运行时配置与持久化

本地 API 地址

background.js 固定维护两个 Cookie 清理目标:http://localhost:36530 与 http://127.0.0.1:36530。README 同时将 36530 说明为内置网易云 API 的默认本地端口。当前已读源码没有显示该端口是否可以通过环境变量或配置文件覆盖,因此不要把它描述成可配置项;对部署和故障排查而言,应优先检查这两个 origin 是否可访问。

Electron Store

主进程创建两个独立的 electron-store 实例:名为 settings 的实例保存设置,名为 window-state 的实例保存窗口状态。getQuitAppPreference() 从设置对象的 other.quitApp 读取退出策略:只有值严格等于 quit 时返回 quit,其余情况都回退为 minimize;如果 Store 初始化或读取失败,同样回退为 minimize。这种保守默认值避免设置损坏导致主进程启动失败。

配置/存储项类型默认/回退作用
settings StoreStoreelectron-store 默认存储保存应用设置
settings.other.quitAppstring非 quit 时按 minimize 处理决定关闭行为偏好
window-state StoreStoreelectron-store 默认存储保存主窗口状态
localhost:36530 / 127.0.0.1:36530URL固定地址本地网易云 API Cookie 管理与服务就绪边界

前端入口约束

根页面没有直接实现业务页面,而是提供 #app 挂载点,并把模块入口交给 /src/main.js。因此,Vite 构建、Electron 加载和 Vue 应用初始化之间必须保持该路径契约;修改入口文件名时需要同步调整 index.html 与构建配置。

html
1<body> 2 <div id="app"></div> 3 <script type="module" src="/src/main.js"></script> 4</body>

Source: index.html

API 与桌面边界

ncm-api-ready-state

这是主进程通过 ipcMain.handle 注册的异步 IPC handler。它不主动启动服务,而是返回 waitForNcmApiReady() 的结果:API 已完成时立即返回 { ready: boolean, error?: string } 形状的 payload;API 尚未完成时等待初始化结果。

这是另一个异步 IPC handler,调用 clearNcmApiCookies()。当 session.defaultSession 不可用时返回 false;可用时分别获取两个本地 API origin 的 Cookie,并使用 Promise.all 并行移除每个 Cookie。单个 origin 的读取/删除异常会被忽略,函数最后返回 true。这意味着“清理请求已执行”与“每一枚 Cookie 都成功删除”不是同一个语义。

check-for-update

该事件使用 ipcMain.on 注册,不返回结果,只将 manualUpdateCheckInProgress 置为 true。源码注释说明它与 src/electron/ipcMain.js 中的处理并存,职责是为设置页面的手动检查更新流程设置标记。

javascript
1ipcMain.handle('ncm-api-ready-state', async () => { 2 return waitForNcmApiReady() 3}) 4ipcMain.handle('ncm-api-cookie-clear', async () => { 5 return clearNcmApiCookies() 6})

Source: background.js

失败模式、边界条件与并发行为

单实例与第二次启动

应用启动立即请求单实例锁。如果锁获取失败,主进程直接调用 app.quit();如果锁获取成功,则监听 second-instance,将已有窗口从最小化或隐藏状态恢复并聚焦。这是桌面播放器很重要的用户体验约束:第二次点击不会创建第二个独立播放实例。

API 启动失败

startNeteaseMusicApi() 的 Promise rejection 会被捕获,错误信息被包装为 { ready: false, error: errorMessage } 并交给 resolveNcmApiReady()。因此 API 失败不会直接让 Electron 主进程退出;调用方可以依据 ready 状态决定如何提示或降级。成功结果也会被归一化:对象结果读取 ready 和可选 error,非对象结果按 { ready: true } 处理。

Cookie 清理函数对两个 origin 分别使用 try/catch;某个 origin 的失败不会阻断另一个 origin,也不会把异常继续抛给 IPC 调用方。代价是调用方只能得到布尔结果,无法知道是哪个 origin 或哪一个 Cookie 删除失败。

退出与平台差异

窗口全部关闭时,macOS 不退出应用,其他平台退出;will-quit 阶段统一注销全局快捷键。Linux 的 MPRIS 模块是延迟加载且带 try/catch 的:仅 Linux 尝试加载,模块不可用时记录警告而继续启动。该设计把可选平台能力的失败隔离在集成层,不阻塞主播放器窗口。

依赖与扩展点

package.json 体现了项目的主要技术边界:Vue/Vite/Electron 构成应用壳与前端基础;@neteasecloudmusicapienhanced/api、axios 支持网易云服务访问;howler、music-metadata、ffmpeg-static、ID3/FLAC 相关库支撑音频播放与媒体处理;electron-store、electron-updater、mpris-service 分别对应桌面持久化、更新和 Linux 媒体控制。

扩展桌面能力时,应优先遵循现有边界:

  1. 在主进程中注册生命周期、窗口或 IPC 行为。
  2. 在渲染端通过既有入口和 IPC/本地 API 使用能力,而不是让 Vue 组件直接承担 Electron 原生对象管理。
  3. 对平台专属依赖采用与 MPRIS 类似的条件加载和失败隔离策略。
  4. 对会影响用户体验的状态(窗口位置、设置、退出策略)使用已有 Store 分层,不与临时进程变量混用。
json
1{ 2 "main": "background.js", 3 "scripts": { 4 "dev": "vite", 5 "build": "vite build", 6 "start": "nodemon --exec electron . --watch ./ --ext js,vue,html,css,json", 7 "dist": "vite build && node scripts/build.js -p never" 8 } 9}

Source: package.json

代码使用示例

主进程启动本地服务

javascript
1app.whenReady().then(async () => { 2 createWindow() 3 startNeteaseMusicApi() 4 .then((result) => { 5 const payload = result && typeof result == 'object' 6 ? { ready: !!result.ready, ...(result.error ? { error: result.error } : {}) } 7 : { ready: true } 8 resolveNcmApiReady(payload) 9 }) 10 .catch((err) => { 11 const errorMessage = err && err.message ? err.message : 'unknown error' 12 resolveNcmApiReady({ ready: false, error: errorMessage }) 13 }) 14})

Source: background.js

单实例锁

javascript
1const gotTheLock = app.requestSingleInstanceLock() 2 3if (!gotTheLock) { 4 app.quit() 5} else { 6 app.on('second-instance', (event, commandLine, workingDirectory) => { 7 if (myWindow) { 8 if (myWindow.isMinimized()) myWindow.restore() 9 if (!myWindow.isVisible()) myWindow.show() 10 myWindow.focus() 11 } 12 }) 13}

Source: background.js

性能与运维注意事项

  • 开发环境需要同时运行 Vite 和 Electron;README 明确开发主窗口使用 http://localhost:5173/,桌面歌词窗口使用 http://localhost:5173/desktop-lyric.html。
  • 本地网易云 API 的启动是异步的,诊断时应先查看 Netease API 已就绪 或 Netease API 启动失败 日志,再判断渲染端功能问题。
  • waitForNcmApiReady() 会缓存首个完成状态,并唤醒排队的等待者;它适合一次性启动门闩,不是持续健康检查机制。当前已读源码没有发现自动重启或定时探活逻辑。
  • Cookie 清理使用 Promise.all 并行删除同一 origin 下的 Cookie,减少清理等待时间,但错误被有意吞掉,运维日志不会提供逐 Cookie 的失败清单。
  • Electron 单实例锁、退出时快捷键注销和平台条件加载是稳定性的关键路径,修改时应优先验证重复启动、窗口关闭、macOS Dock、Linux MPRIS 不可用等场景。
  • README.md:产品定位、功能总览、安装、开发与发行说明。
  • package.json:脚本、运行时入口和依赖清单。
  • background.js:Electron 主进程、窗口、IPC 和本地 API 启动逻辑。
  • index.html:Vite/Vue 渲染端 HTML 入口。