Files
StarlightRadio/HANDOFF.md
T
Xiao-no-love 16df0a7d08 v2.4: UI 规范化(token体系) + 复制直播链接 + 设置页间距
- 建立完整 token 体系: 色板/字号阶梯(9档)/圆角阶梯/阴影/动效/间距
- 字号 42→9 种、圆角 20→8 种、正文硬编码色清零
- 补丁区(v2/v2.1/v2.2 追加段)并入主体系: 统一缩进 + 全用 token
- header「打开直播流」改为「复制直播流链接」(navigator.clipboard + 降级)
- header 图标按钮统一为胶囊形(20px 圆角)
- 设置页: .page.active 加 gap, section-body 内边距对齐, 缓解拥挤

测试: api 16/16, e2e 17/17, write 5/5 全绿
2026-09-24 22:20:55 +08:00

22 KiB
Raw Blame History

交接文档 · 音乐在线电台 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. 关键设计决策(勿轻易推翻)

  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 / 或新起)跑:

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 对照(改前端时用)

  • 头部: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. 补多路流并发压力测试

13. 用户视角 · 待优化点(下一会话可选)

站在真实使用者角度梳理,按"体验影响"排序。均未实施,需老板定优先级。

高价值(体验硬伤)

  1. 缺"电台收听页" —— 现在 liveLink 只打开裸 .m3u8,浏览器无法直接播放。应在站内加一个 HLS.js 播放页(选频道→收听直播),否则"电台"没法在浏览器里直接听。
  2. 面板无"正在播放"实时卡片 —— 打开面板不知道电台此刻放到哪首。SSE 已推 now-playing/song-finished,前端只 loadPlaylist(),应加一个醒目的 now-playing 展示(封面+歌名+进度)。

中价值

  1. 设置页无 UI —— /api/system/settings、/api/system/config 已就绪,前端没有对应界面。
  2. 导入上传无进度百分比 —— 现只有"上传中..."文字,fetch 不支持上传进度,需换 XHR + upload.onprogress。
  3. 缓存生成无进度 —— 导入后只能看到"生成中/已缓存"两态,无进度条。
  4. 搜索无分页/加载更多 —— searchNetEase 固定 limit 20。

低价值 / 打磨

  1. 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.m3u8 15 段滑动窗口,切片可达。

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 改动清单

  1. 频道管理+切换合并为单一入口:
    • 删除 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。
  2. 去 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 味)。
  3. 测试更新: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 味」细节。

17. v2.4 UI 规范化 / Token 体系(已完成 2026-09-24)

老板反馈「整体 UI 风格很撕裂」→ 全量重构 style.css 的 token 与取值体系。

17.1 问题诊断

style.css 原是两段拼接:主体系(1~1730,16 空格缩进、有分节) + 补丁区(1731+,不缩进、硬编码色的 v2/v2.1/v2.2 追加)。具体失控:

  • 字号 42 种(0.54~2.4rem 密密麻麻,还混用 0.82rem/.82rem 写法)
  • 圆角 约 20 种(3/5/6/7/8/9/10/11/12/14/16/18/20/22/28px,定义了 --radius-* 却大半没用)
  • 颜色硬编码:55 hex + 38 rgba,兜底色 #1f6feb 出现 8 次(带 var(--accent,#1f6feb) fallback)

17.2 重构内容

建立完整 token 体系(:root 重写):

  • 色板:--bg/surface/white/accent*/text*/border*/danger*/success*/warning*/overlay/log-*
  • 字号阶梯 9 档:--fs-2xs/…/--fs-4xl
  • 圆角阶梯:--radius-xs/sm/base/lg/xl/pill
  • 阴影:--shadow-xs/sm/base/lg/accent
  • 动效:--dur-fast/base/slow + --ease-out/spring
  • 间距:--sp-1~--sp-6

全局收敛:

  • font-size:42 种 → 9 种(67 处替换为 var)
  • border-radius:20 种 → 8 种(28 处替换为 var,余 50%/xl 特殊)
  • 硬编码色:正文区 清零(全转 var)
  • accent 阴影重复值收进 --shadow-accent;#fff→--white;#fdd→--danger-hover
  • 补丁区整体并入主体系:统一 12 空格缩进、改用 --fs-*/--radius-*/--sp-*、重写分节注释

17.3 关键取值变化(有意微调,贴近原值)

  • --radius-sm: 12px→10px,--radius: 16px→14px,--radius-lg: 24px→20px,--radius-xl: 28px 保留
  • 补丁区硬编码圆角 14/18/22px → 归入 --radius/--radius-lg
  • live-cover-ph 大图标 2.4rem → --fs-4xl

17.4 验证

  • 全文括号配平({} 360/360、( ) 581/581),无 var 残渣/嵌套。
  • 回归:api 16/16、e2e 17/17、write 5/5 全绿。
  • node --check app.js 通过。

17.5 待老板浏览器实测

  • 整体观感是否统一(字号阶梯、圆角、配色)。
  • 特别注意:卡片圆角、按钮胶囊、模态框圆角的和谐度。

17.6 保留项(非撕裂)

  • 毛玻璃 rgba(255,255,255,…) 背景、阴影 rgba:正当设计值。
  • login.html 独立暗色主题(自己的 :root)。
  • body 字体栈、--log-bg 终端风格:保留。