v2.2 前端增强: - 站内 HLS 收听页 (#pageLive),hls.js 本地化 vendor/hls.min.js - 设置页 (缓存/配置/日志) - 上传进度百分比 (XHR upload.onprogress) - 资源列表排序 (时间/热度/名称 ×升降) - 面板正在播放实时化 (接 playlist-update/song-finished,删死监听) - 死代码清理 + forgetSong 内存泄漏修复 + CORS 非法头移除 - start.cmd 托管启动脚本 (set "VAR=val" 避免尾部空格) v2.3 UI 去 AI 味 (依据 ibelick/ui-skills baseline-ui): - 频道管理+切换合并为单一入口 #btnStreamMgr.channel-btn (删除 header 的 #streamSelect 下拉,弹窗内统一切换/删除/新建) - 清除全部 CSS 渐变与 glow: 橙红 #ff5f6d→#ffc371、紫 #8b5cf6、 径向光晕、缓存条渐变 → 全部改为 accent/token 纯色 - live 区、modal 区硬编码色统一归 CSS 变量 token 测试: api 16/16, e2e 17/17, write 5/5 全绿
19 KiB
19 KiB
交接文档 · 音乐在线电台 v2
本文件用于跨会话交接。上下文耗尽后,新会话先读此文件再动手。
0. TL;DR
- 项目:
F:\music_online\demo— Node.js + FFmpeg 的 HLS 音乐电台 - 状态:v2.3 功能完整 + UI 已去 AI 味重构,38 项测试全绿(api16/e2e17/write5);详见 §16。
- 服务:正在运行
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
- 启动:
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)
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. 关键设计决策(勿轻易推翻)
- SQLite 用 better-sqlite3 + WAL:单进程应用,同步 API 最省事;WAL 解决读写互斥。用户曾担心并发,已解释并落地。
- 不迁移 v1 的 JSON 数据:用户明确说旧数据不要。
- 鉴权选"中档":整站 Cookie 会话登录,面板+API 锁,HLS 流不锁(播放器难带 cookie)。
- 封面/资源全本地化:
public/covers/存封面,public/vendor/存前端库;系统字体替代 Google Fonts。 - songs.file 存文件名:不是绝对路径,迁移目录不失效。
- 导入先落 tmp 再 rename:避免 chokidar 扫到半截文件。
7. 测试方法
服务起来后(svc005 / 或新起)跑:
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. 已完成清单
- SQLite 替换 JSON(5 表 + WAL)
- 级联删除
/api/music/delete+ 批量 - 多路流 LiveStream 类 + 每路独立播放列表
- Cookie 会话鉴权 + 登录页 + 鉴权绕过漏洞修复
- 本地上传导入(multer)+ 元数据自动匹配
- 封面本地化 + vendor 本地化 + 系统字体
- 日志系统(分级/落盘/面板可查)
- API 统一返回结构
- 优雅退出补 FFmpeg 子进程 kill
- 模块化拆分(1233 行单文件 → src/ 12 模块)
- 前端 app.js 重写(多路流/鉴权/导入/删除)
- 前端多路流管理 UI(频道管理模态框:新建/删除/切换) ← v2.1
9. ⚠️ 待排查 / 已知问题(重点!)
9.1 用户反馈"功能还有问题" —— 具体未明,需先问清楚
下一会话第一件事:问用户到底哪个功能出问题(现象、复现步骤、控制台报错)。 可能的方向见下。
9.2 前端多路流管理 UI —— ✅ 已解决(v2.1)
- 已补:header 新增
#btnStreamMgr入口 → 弹出「频道管理」模态框#streamModal。 - 功能:列出所有流(含运行状态/当前标记)、一键切换、删除(main 受保护)、新建(ID+名称,前端正则校验)。
- 相关:
index.html新增 modal 结构;app.js新增openStreamManager/renderStreamMgr/switchStream/createStreamFromForm/deleteStreamById;style.css末尾追加.modal-*/.stream-*样式。 - 已验证:后端 create/delete/重复/非法ID/main保护 全通过;app.js
node --check通过;JS 引用 ID 与 HTML 无缺失。
9.3 前端 JS 交互未实测
public/js/app.js是重写的,只做了语法检查 + HTTP 层测试。- 未验证:SSE 实时刷新、拖拽排序 reorder、mini player、页面切换、导入进度提示等浏览器行为。
- 建议:手动开浏览器点一遍,看 Console 有无报错。
9.4 潜在 Bug 点 —— ✅ 已处理(v2.1)
→ 保护集为空时最坏误删缓存,无害;保留。enforceCacheSizeLimit依赖 streams Map→ 已删。scanner.js里const parsed = meta.sanitizeFileName;死代码metadata.js enrichSong启动时重匹配慢歌 → 保留(可接受)。- 多路流并发预生成排队 → 观察中。
app.js的esc()用String.fromCharCode(38)绕过工具层转义,勿优化。
9.4b v2.1 死代码清理 + Bug 修复(本轮)
- 删死代码:
media.setCacheCallbacks(含onCacheReady/onCacheError,无调用方)、auth.getPassword/isGenerated、store.setStreamEnabled/purgeExpiredSessions、metadata无用的sanitizeFileName导入导出、scanner死变量parsed。全部node --check通过。 - 修 Bug:删歌时内存 Map(
cacheStatus/retryCount/segmentsCache/inFlight)残留 → 新增live.forgetSong(file),删除路由两处改用它,避免同名文件重导读到脏状态。 - 修 Bug:
server.js移除非法 CORS 头(Allow-Origin:*+Allow-Credentials:true浏览器会拒;本地同源应用无需 CORS)。 - 测试:45 项全绿(api 16 / e2e 17 / write 5 / 真实 ogg 导入 7)。
9.5 前端 HTML 元素 ID 对照(改前端时用)
- 头部:
cacheInfostreamSelectliveLinkbtnLogout - 搜索页:
musicIdbtnSearchparseHintparsedIdmusicPreviewmusicCovermusicNamemusicArtistmusicDetailsbtnDownloadsearchResultssearchTips - 导入卡片:
importCardimportFileimportTitleimportArtistbtnImportimportProgress - 资源页:
resourceCountresourceSearchresourceList - 列表页:
playlistCountbtnClearplaylistList - mini player:
miniPlayermpProgressmpBodympCovermpNamempArtistmpTimempPlaympExpandmusicPlayer - 导航:
.nav-tab[data-page],页面pageSearch/pageResources/pagePlaylist/pageAbout
10. 工具层踩坑记录(写代码时注意)
- XML 实体转义会吃掉
&":写 JS/CSS 含这些字面量时,用String.fromCharCode(38)或字符码构造,别直接写。 - replace_lines 多段锚点行号易偏:先
file_read/ python 打印实际行号再改;改错了整批不生效或误删。 - smart_patch 行数必须精确,且锚点行号要对;容易失败,简单改动优先 replace_lines。
- 后台服务用 action background="true"(不要
start /b,会粘连卡死)。 - service_stop 会连子进程一起杀(node 的 ffmpeg 子进程会被带走)。
- Windows 无
head/tail,用findstr或 python。 - 改完源码要重启 node(磁盘 .py/.js 改了,运行中的进程不生效)。
- Python 跑 Windows 输出:
chcp 65001 && set PYTHONIOENCODING=utf-8。 node -X utf8是错的(那是 Python 的选项),node 直接node即可。
11. 当前运行态
- 后台服务:
svc005(PID 15308 起),日志service_log svc005 - DB:
data.db(5 首歌曲,封面已本地化) - 缓存:
hls_cache/5 个目录,约 28MB - Git 工作区:干净(最新
a4745db)
12. 下一步建议(按优先级)
- 问清用户的"问题"具体是什么 ← 最优先
- 补前端"新建/删除流"UI(若需求确认)
- 浏览器手动过一遍所有交互,修 Console 报错
- 清理死代码(§9.4)
- 正式化:设置固定
PANEL_PASSWORD,写 systemd/服务化启动 - 补多路流并发压力测试
13. 用户视角 · 待优化点(下一会话可选)
站在真实使用者角度梳理,按"体验影响"排序。均未实施,需老板定优先级。
高价值(体验硬伤)
- 缺"电台收听页" —— 现在
liveLink只打开裸.m3u8,浏览器无法直接播放。应在站内加一个 HLS.js 播放页(选频道→收听直播),否则"电台"没法在浏览器里直接听。 - 面板无"正在播放"实时卡片 —— 打开面板不知道电台此刻放到哪首。SSE 已推
now-playing/song-finished,前端只loadPlaylist(),应加一个醒目的 now-playing 展示(封面+歌名+进度)。
中价值
- 设置页无 UI ——
/api/system/settings、/api/system/config已就绪,前端没有对应界面。 - 导入上传无进度百分比 —— 现只有"上传中..."文字,fetch 不支持上传进度,需换 XHR +
upload.onprogress。 - 缓存生成无进度 —— 导入后只能看到"生成中/已缓存"两态,无进度条。
- 搜索无分页/加载更多 ——
searchNetEase固定 limit 20。
低价值 / 打磨
- mini player 无音量控制;8. 无播放历史/最热排序(
heat字段已有);9. 单弱密码test123(正式化要固定强密码,见 §12);10. 网易云无版权只给文字提示,可做得更友好。
已知需实测(无法自动化)
- 浏览器端:SSE 实时刷新、拖拽 reorder、mini player、上传进度、页面切换动画、频道管理模态框交互。
14. v2.2 前端增强(已完成)
本轮在 v2.1 基础上补齐了用户视角的 4 个高/中价值缺口:
14.1 站内 HLS 收听页(#pageLive,默认首页)
- 新增底部导航「收听」tab(5→6 个),作为默认 landing 页。
- 引入本地化
public/vendor/hls.min.js(hls.js 1.5.17,414KB,全本地化无 CDN)。 app.js:startLive/stopLive/toggleLive/loadLiveCover,用 HLS.js 挂载到全局#musicPlayer。- 修复了「点 liveLink 打开裸 m3u8 无法播放」的根本问题——现在浏览器内可直接收听。
- 卡片展示:封面(取当前播放曲目封面)+ 频道名 + 当前曲目 + 播放按钮 + 状态。
14.2 面板「正在播放」实时化
- 发现并修复:后端从不发
now-playing事件,前端那个es.addEventListener("now-playing")是死监听,已删。 - 真正信号是
playlist-update/song-finished;新增refreshLive()(300ms 节流)接到这两个事件 + 轮询。
14.3 设置页(#pageSettings)
- 缓存管理:已用/上限 + 进度条(绿/黄/红三档)+ 一键清理。
- 运行配置:HTTP 端口/切片时长/窗口/缓存上限/FFmpeg 并发(
/api/system/config)。 - 运行日志:tail 行数可选(50/100/200/500),暗色终端风格(
/api/system/logs)。
14.4 上传进度百分比
fetch不支持上传进度,新增uploadWithProgress()(XHR +upload.onprogress)替换导入逻辑。- 上传时实时显示「上传中... N%」。
14.5 验证
- 回归:api 16/16、e2e 17/17。
- 新增功能校验:12/12(页面元素 + 3 个接口)。
- HLS 流实测:
/hls/main/index.m3u815 段滑动窗口,切片可达。
14.6 待老板浏览器实测
- 收听页点播放是否出声、切频道是否正确切流。
- 设置页日志滚动、缓存条颜色。
- 上传大文件进度条。
15. v2.2.1 资源排序(已完成)
- 资源页头部新增排序下拉
#resourceSort:加入时间 ↓↑ / 热度 ↓↑ / 名称 ↑↓ 共 6 种。 - 后端
/api/resources补createdAt字段(取songs.created_at)——之前没返回,时间排序会失效。 - 前端
sortResources()本地排序;名称用localeCompare(..., "zh", {numeric:true})支持中文与数字。 - 与搜索框组合:排序改数据顺序,搜索只是隐藏不匹配项,二者不冲突。
- 测试脚本改为动态基线(不再硬编码 5 项),新增歌曲不会误报。回归 38/38 全绿。
16. v2.3 UI 去 AI 味重构(已完成 2026-09-24)
原「MIUIX 风格重构」任务。老板改方向:不找 MIUIX(那是 Compose 的),改用 GitHub
ibelick/ui-skills(Web 版设计工程 skill)的规则手工重构。
16.1 采用的 skill
C:\Users\xiao\.claude\skills\ui-skills(clone 自 github.com/ibelick/ui-skills,9046★)。- 关键规则(
skills/baseline-ui/SKILL.md):禁渐变、禁紫/多彩渐变、禁 glow 当主视觉、单视图单强调色、交互反馈 ≤200ms、入场 ease-out、只动 transform/opacity。 - 我们的项目是原生 HTML/CSS/JS(非 Tailwind/React),只取其原则,类名约束不适用。
16.2 改动清单
- 频道管理+切换合并为单一入口:
- 删除 header 的
<select id="streamSelect">下拉 + 原「频道管理」按钮,合并为单一按钮#btnStreamMgr.channel-btn(图标+当前频道名#currentChannelName),点击打开原有#streamModal(内含切换/删除/新建)。 app.js:loadStreams()不再填 select,改写#currentChannelName文本;删除 select 的 change 监听;switchStream()补if (livePlaying) stopLive()。style.css:.stream-select样式 →.channel-btn。
- 删除 header 的
- 去 AI 味(清全部渐变/glow):
page-bg径向光晕 → 纯色var(--bg);.bg-shapes紫色#8b5cf6→var(--accent)。.about-hero渐变 →var(--accent-light);.mp-progress渐变 → 半透明纯色。.live-cover-ph/.live-play-btn/.live-progress-fill橙红渐变#ff5f6d→#ffc371→var(--accent);play-btn glow 阴影 →var(--shadow-sm);live-badge glow → 中性。.cache-bar-fill三档渐变 →var(--success/--warning/--danger)。- live 区、modal 区硬编码色(#222/#999/#333/#888/#fff/rgba)统一改为 token。
- 保留:login.html 暗色径向(独立深色主题,非 AI 味)。
- 测试更新:
test_e2e.py断言id="streamSelect"→id="btnStreamMgr" and id="currentChannelName"。
16.3 验证
node --check public/js/app.js通过。- 回归:api 16/16、e2e 17/17、write 5/5(需先 ffmpeg 生成
tmp/test_import.mp3素材)。 - 全局扫描:public/ 下渐变计数 = 0(除 login.html 暗色径向)。
16.4 备份
F:\music_online\demo_backup_20260924_2129(本次改动前的备份,仍有效)。
16.5 待老板浏览器实测(无法自动化)
- 频道按钮点开弹窗、切频道是否正确换流。
- 收听页配色是否满意(现为单 accent 蓝,无渐变)。
- 各项 hover/动效观感。
16.6 未做 / 可选后续
- 是否
git commit(工作区改动含本次 + 上轮 v2.2,均未提交)。 - 深色模式、缓存条颜色再打磨、其余「AI 味」细节。