feat: 声匣 —— 网易云 / QQ 音乐自托管下载服务

由 163-music-dl CLI 重构为 Web 服务:
- 下载流水线、歌词与标签逻辑沿用旧版(node-id3 / ffmpeg)
- 网易云(chksz 接口 + music.163.com 元数据)与 QQ 音乐(VIP Cookie)provider
- QQ 扫码登录与 musickey 自动续期
- SQLite 持久化的下载队列,SSE 实时进度
- 曲库工具(M3U / ZIP / 回收站 / 补全标签)、历史统计与导出、访问密码
- Vite 8 + Vue 3 前端,浅色 / 深色
- Docker 部署配置(飞牛 NAS)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jocay
2026-10-10 22:05:30 +08:00
commit 21945cc54f
73 changed files with 12507 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
# CLAUDE.md
声匣:自托管音乐下载 Web 服务(网易云 + QQ 音乐),本机 `npm start` 或 NAS Docker 运行。UI、注释、日志均为中文。
## 命令
- `npm run dev` — 后端 `node --watch server/src/main.ts`(:8066)+ Vite(:5173,代理 `/api`)
- `npm start` — 生产模式(需先 `npm run build`)
- `npm run typecheck` — `tsc`(server)+ `vue-tsc`(web)。改动后必须通过
- `npm run smoke -- <id|链接> [quality] [album|flat]` — 端到端跑一次下载流水线(会导入 QQ Cookie)
没有测试框架;用 smoke、curl `/api/*`,以及无头浏览器截图验证。测试下载请用 `MUSIC_DIR` / `DATA_DIR` 指向临时目录,不要污染 `./music` 与 `./data`。
## 架构
单个 package.json:
- `server/src/` — Koa 3,**Node 原生 TypeScript 类型剥离直接运行,无构建步骤**
- 只能用可擦除语法(`erasableSyntaxOnly`):不能用 enum / namespace / 参数属性
- 相对导入必须写 `.ts`;类型导入用 `import type`(`verbatimModuleSyntax`)
- `core/` 与平台无关:`pipeline.ts`(下载流水线)、`tagging.ts`、`lyrics.ts`、`layout.ts`、`quality.ts`、`net/http.ts`
- `core/providers/{netease,qq}/` 实现 `MusicProvider`(`core/types.ts`),在 `providers/index.ts` 注册;`resolveLink` 处理分享短链
- QQ:`qq/client.ts` 的 `musicu()` 在 comm 中带 `uin` + `authst` 并附 Cookie —— 缺这一步拿不到 VIP 音源
- `queue.ts` — 任务存 SQLite `tasks` 表,进度只在内存,经 `events.ts` 的 bus 推送;`routes/api.ts` 的 `/api/events` 是 SSE
- `accounts.ts`(Cookie 用 `secret.ts` AES-GCM 加密存储)、`settings.ts`(运行时设置)、`auth.ts`(可选访问密码)、`library.ts`(曲库工具)、`stream.ts`(试听代理)
- `db.ts` — `node:sqlite`;history / tasks / settings 表
- `web/` — Vite 8 + Vue 3.5 + Vue Router 5,无 UI 库
- `lib/store.ts` 全局状态 + SSE;`lib/player.ts` 试听;`lib/api.ts` 类型与请求
- `components/TrackList.vue` 是所有曲目列表(搜索 / 专辑 / 歌单 / 歌手)的核心
- 样式只用 `styles/tokens.css` 的 CSS 变量;`base.css` 有 `.page` `.btn` `.seg` `.badge` 等通用类
## 约定
- 统一音质键 `standard|high|lossless|hires|master|spatial|effect`;provider 映射到平台参数,不可用时按 `fallbackChain` 降级。QQ 只声明到 hires
- 目录:`album` 布局 = `{第一位专辑艺人}/{专辑}/{NN}. {歌名}.ext`(与 NAS 曲库一致);`flat` = `{歌手} - {歌名}.ext`
- 下载先写 `.part` 再改名;已存在判定:≥ 音源 99% 且 ≤ 音源 + 8MB(内嵌封面会变大)
- `FatalError` 表示重试无意义;队列间隔 `gapMs`(默认 400ms)防止打爆上游
- 服务端永远不把 Cookie 原文返回前端;`relFile` 等路径一律用 `/`
- 传给 TrackList 的数组必须稳定(用 computed),否则每次渲染都会清空选择
- 前端 `ref<null>` 的接口要返回对象包一层(Koa 对 `null` body 返回 204)
- TypeScript 固定 6.x:TS 7 没有 JS API,vue-tsc 无法使用
- 网易云红 / QQ 绿已用 dataviz 校验器验证(深色 QQ 绿 `#42a07d`);二者 CVD 区分度在 6–8,必须配合文字标签使用