Files
StarlightRadio/HANDOFF.md
T

247 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 交接文档 · 音乐在线电台 v2
> 本文件用于跨会话交接。上下文耗尽后,新会话**先读此文件**再动手。
---
## 0. TL;DR
- **项目**:`F:\music_online\demo` — Node.js + FFmpeg 的 HLS 音乐电台
- **状态**:v2 重构**基本完成**,后端 38/38 测试通过;**但用户反馈"功能还有问题",具体未明**(见 §9 待排查)
- **服务**:正在运行 `svc005`,`http://127.0.0.1:3000`,密码 `test123`(测试用)
- **Git**:`ea5aa0c`(基线) → `b8e9bf3`(v2主体) → `a4745db`(收尾)
---
## 1. 运行 / 环境
- 环境:Windows 11,Node v24.5.0,npm 11.5.1,FFmpeg 9.0.1(含 ffprobe,均在 PATH),Python 3.11
- 启动:
```cmd
cd /d F:\music_online\demo
set PANEL_PASSWORD=test123 && node index.js
```
- 依赖:`better-sqlite3`、`multer`、`chokidar`、`express`(已装)
- 登录:浏览器开 `http://127.0.0.1:3000`,输入密码
---
## 2. 目录结构(v2)
```
demo/
├── index.js # 入口,仅 require("./src/server").start()
├── package.json # v2.0.0
├── data.db # SQLite(自动生成,WAL)
├── HANDOFF.md # 本文件
├── README.md # v2 说明
├── src/
│ ├── server.js # 装配:中间件、路由、启动、优雅退出
│ ├── config.js # 所有配置(环境变量解析)
│ ├── logger.js # 分级日志 + 落盘 + 内存环形缓冲(tail)
│ ├── db.js # SQLite 初始化 + schema(5表) + WAL
│ ├── store.js # 数据访问层,所有 SQL 收敛于此
│ ├── util.js # fileHash/解析文件名/原子写/目录大小 等
│ ├── media.js # ffprobe + FFmpeg 切片 + 并发队列 + 进程追踪
│ ├── metadata.js # 网易云 API + 封面下载本地化 + 元数据补全
│ ├── livestream.js # LiveStream 类 + 多路流管理 + 缓存状态/清理
│ ├── scanner.js # audio/ 监听 + 同步 DB
│ ├── auth.js # Cookie 会话鉴权 + requireAuth 中间件
│ ├── sse.js # Server-Sent Events 广播
│ └── routes/
│ ├── auth.js # /api/auth/*
│ ├── music.js # /api/music/* (搜索/详情/下载/导入/删除/enrich)
│ ├── resources.js # /api/resources
│ ├── streams.js # /api/streams/* (流管理 + 播放列表)
│ └── system.js # /api/system/* (缓存/日志/配置/设置)
├── public/
│ ├── index.html # 主面板
│ ├── login.html # 登录页(新)
│ ├── css/style.css # 含 v2 追加样式(.stream-select/.import-card/.now-badge)
│ ├── js/app.js # v2 重写:多路流/鉴权/导入/删除
│ ├── vendor/ # 本地化:bootstrap/icons/sortable + woff2
│ ├── covers/ # 本地化封面(每首一个 jpg)
│ └── favicon.ico/.png
├── scripts/fetch_vendor.py # 一次性下载前端 CDN 资源(已跑过)
├── audio/ # 音频(5 首 mp3,已 gitignore)
├── hls/ # 直播输出 hls/{streamId}/index.m3u8
├── hls_cache/ # 切片缓存 hls_cache/{fileHash}/
├── tmp/uploads/ # multer 上传临时目录
└── test_api.py / test_e2e.py / test_write.py / test_enrich.py # 测试脚本
```
---
## 3. 数据模型(SQLite, src/db.js)
```sql
songs(id, file UNIQUE, name, artist, cover, duration, file_size,
heat, cache_status, created_at)
-- file 存「文件名」不是绝对路径(可移植)
-- cover 存 web 路径如 /covers/xxx.jpg
streams(id PK, name, enabled, current_pos, media_seq, created_at)
-- 默认流 id='main',不可删除
stream_playlist(id PK, stream_id, song_id, position)
-- 每路流独立播放列表,外键 CASCADE
settings(key PK, value)
sessions(token PK, created_at, expires_at)
```
**关键**:切片缓存 `hls_cache/{md5(文件名)}/` 与「流」无关,多路共享,省空间。
---
## 4. API 清单
除 `/api/auth/*`、`/hls`、`/hls_cache`、静态资源、`/login` 外,**全部需登录**。
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/auth/login` | `{password}` → 设 cookie |
| POST | `/api/auth/logout` | 登出 |
| GET | `/api/auth/me` | 登录状态 |
| GET | `/api/resources` | 资源列表(含 cacheSize/cover) |
| GET | `/api/music/search?keyword=` | 网易云搜索 |
| POST | `/api/music/detail` | `{id}` |
| POST | `/api/music/download` | `{id,name,artist,albumImage}` |
| POST | `/api/music/import` | **multipart**:audio + title + artist |
| POST | `/api/music/delete` | `{file}` 级联删 |
| POST | `/api/music/delete-batch` | `{files:[]}` |
| POST | `/api/music/enrich` | `{file, skipMatch?}` 重匹配元数据 |
| GET | `/api/streams` | 流列表 |
| POST | `/api/streams` | `{id,name}` 创建 |
| DELETE | `/api/streams/:id` | 删除(main 不可删) |
| POST | `/api/streams/:id/start\|stop` | 启停 |
| GET | `/api/streams/:id/playlist` | 播放列表(含 entryId/isCurrent) |
| POST | `/api/streams/:id/playlist/add` | `{file}` |
| POST | `/api/streams/:id/playlist/remove` | `{entryId}` |
| POST | `/api/streams/:id/playlist/reorder` | `{order:[entryId...]}` |
| POST | `/api/streams/:id/playlist/clear\|next\|prev\|jump` | |
| GET | `/api/system/cache` | 缓存占用 |
| POST | `/api/system/cache/cleanup` | 手动清理 |
| GET | `/api/system/logs?tail=` | 日志 |
| GET | `/api/events` | SSE(需 cookie 鉴权) |
**统一返回**:`{ok:true, ...}` 或 `{ok:false, error:"..."}`。
---
## 5. 直播流地址
- 默认:`/hls/main/index.m3u8`
- 其它:`/hls/{streamId}/index.m3u8`
- 切片:`/hls_cache/{hash}/indexN.ts`
---
## 6. 关键设计决策(勿轻易推翻)
1. **SQLite 用 better-sqlite3 + WAL**:单进程应用,同步 API 最省事;WAL 解决读写互斥。用户曾担心并发,已解释并落地。
2. **不迁移 v1 的 JSON 数据**:用户明确说旧数据不要。
3. **鉴权选"中档"**:整站 Cookie 会话登录,面板+API 锁,**HLS 流不锁**(播放器难带 cookie)。
4. **封面/资源全本地化**:`public/covers/` 存封面,`public/vendor/` 存前端库;系统字体替代 Google Fonts。
5. **songs.file 存文件名**:不是绝对路径,迁移目录不失效。
6. **导入先落 tmp 再 rename**:避免 chokidar 扫到半截文件。
---
## 7. 测试方法
服务起来后(svc005 / 或新起)跑:
```cmd
cd /d F:\music_online\demo
set PYTHONIOENCODING=utf-8
python test_api.py # 16 项:鉴权/资源/流/缓存/日志
python test_e2e.py # 17 项:静态资源/登录/首页/HTML元素
python test_write.py # 5 项:导入 + 删除(用 tmp 副本,不动原曲)
python test_enrich.py # 给 5 首补封面(已跑过,5/5 成功)
```
上次全绿:**38/38**。但**注意**:这些只测了 HTTP 层,**浏览器端 JS 交互没自动化测过**。
---
## 8. 已完成清单
- [x] SQLite 替换 JSON(5 表 + WAL)
- [x] 级联删除 `/api/music/delete` + 批量
- [x] 多路流 LiveStream 类 + 每路独立播放列表
- [x] Cookie 会话鉴权 + 登录页 + 鉴权绕过漏洞修复
- [x] 本地上传导入(multer)+ 元数据自动匹配
- [x] 封面本地化 + vendor 本地化 + 系统字体
- [x] 日志系统(分级/落盘/面板可查)
- [x] API 统一返回结构
- [x] 优雅退出补 FFmpeg 子进程 kill
- [x] 模块化拆分(1233 行单文件 → src/ 12 模块)
- [x] 前端 app.js 重写(多路流/鉴权/导入/删除)
---
## 9. ⚠️ 待排查 / 已知问题(重点!)
### 9.1 用户反馈"功能还有问题" —— **具体未明,需先问清楚**
> 下一会话**第一件事**:问用户到底哪个功能出问题(现象、复现步骤、控制台报错)。
> 可能的方向见下。
### 9.2 前端多路流管理 UI **没做**
- 后端能创建/删除流,但前端只有一个**切换下拉框**(`#streamSelect`),**没有"新建流/删除流"的界面**。
- 若用户说"多路流用不了",大概率指这个。
### 9.3 前端 JS 交互未实测
- `public/js/app.js` 是重写的,只做了语法检查 + HTTP 层测试。
- **未验证**:SSE 实时刷新、拖拽排序 reorder、mini player、页面切换、导入进度提示等浏览器行为。
- 建议:手动开浏览器点一遍,看 Console 有无报错。
### 9.4 潜在 Bug 点(未确认)
- `livestream.js` 的 `enforceCacheSizeLimit` 依赖 `streams` Map,若在 `loadStreams` 前被调用可能拿到空集。
- `scanner.js` 里 `const parsed = meta.sanitizeFileName;` 是**死代码**(无用变量),无害但可清理。
- `metadata.js` 的 `enrichSong` 每次启动会对缺元数据的歌重新请求网易云,网络慢会拖启动。
- 多路流同时播放不同歌时,FFmpeg 预生成队列(并发默认 3)可能跟不上,需观察。
- `app.js` 里 `esc()` 用了 `String.fromCharCode(38)` 拼接,是**为了绕过工具层的 XML 实体转义**,不要"优化"回字面量 `&`,否则写文件时会被工具吃掉。
### 9.5 前端 HTML 元素 ID 对照(改前端时用)
- 头部:`cacheInfo` `streamSelect` `liveLink` `btnLogout`
- 搜索页:`musicId` `btnSearch` `parseHint` `parsedId` `musicPreview` `musicCover` `musicName` `musicArtist` `musicDetails` `btnDownload` `searchResults` `searchTips`
- 导入卡片:`importCard` `importFile` `importTitle` `importArtist` `btnImport` `importProgress`
- 资源页:`resourceCount` `resourceSearch` `resourceList`
- 列表页:`playlistCount` `btnClear` `playlistList`
- mini player:`miniPlayer` `mpProgress` `mpBody` `mpCover` `mpName` `mpArtist` `mpTime` `mpPlay` `mpExpand` `musicPlayer`
- 导航:`.nav-tab[data-page]`,页面 `pageSearch/pageResources/pagePlaylist/pageAbout`
---
## 10. 工具层踩坑记录(写代码时注意)
1. **XML 实体转义会吃掉 `&` `"`**:写 JS/CSS 含这些字面量时,用 `String.fromCharCode(38)` 或字符码构造,别直接写。
2. **replace_lines 多段锚点行号易偏**:先 `file_read` / python 打印实际行号再改;改错了整批不生效或误删。
3. **smart_patch 行数必须精确**,且锚点行号要对;容易失败,简单改动优先 replace_lines。
4. **后台服务用 action background="true"**(不要 `start /b`,会粘连卡死)。
5. **service_stop 会连子进程一起杀**(node 的 ffmpeg 子进程会被带走)。
6. **Windows 无 `head`/`tail`**,用 `findstr` 或 python。
7. **改完源码要重启 node**(磁盘 .py/.js 改了,运行中的进程不生效)。
8. **Python 跑 Windows 输出**:`chcp 65001 && set PYTHONIOENCODING=utf-8`。
9. **`node -X utf8` 是错的**(那是 Python 的选项),node 直接 `node` 即可。
---
## 11. 当前运行态
- 后台服务:`svc005`(PID 15308 起),日志 `service_log svc005`
- DB:`data.db`(5 首歌曲,封面已本地化)
- 缓存:`hls_cache/` 5 个目录,约 28MB
- Git 工作区:干净(最新 `a4745db`)
---
## 12. 下一步建议(按优先级)
1. **问清用户的"问题"具体是什么** ← 最优先
2. 补前端"新建/删除流"UI(若需求确认)
3. 浏览器手动过一遍所有交互,修 Console 报错
4. 清理死代码(§9.4)
5. 正式化:设置固定 `PANEL_PASSWORD`,写 systemd/服务化启动
6. 补多路流并发压力测试