| URL 拼接 |
f"{API}/{path.lstrip('/')}",API 常量忘加尾斜杠会拼成 api.bilibili.comx |
bilimuse/providers/bilibili.py:94 |
| Wbi 签名 |
播放接口需 wbi 签名 + buvid3 cookie 防 -412 |
bilimuse/providers/bilibili.py:76 |
| wbi 搜索被风控 |
wbi 搜索端点未登录返回 v_voucher 无结果,用旧版 search/type |
bilibili.py:112 |
| httpx 系统代理 |
httpx 0.28 在 Windows 读注册表 WinINET 代理,需 trust_env=False |
bilibili.py:34 |
| 控制台乱码 |
$env:PYTHONIOENCODING='utf-8',数据本身 UTF-8 正确 |
仅显示层 |
| typer 0.27 Exit |
Exit 只有 code 参数,错误信息先 echo 再 Exit(code=1) |
bilimuse/cli.py:79 |
| 下载前查 ffmpeg |
需转码时先 shutil.which 再下载,避免半途失败 |
bilimuse/services/download.py:87 |
| SQLite 去重 |
UNIQUE(bvid,cid) + INSERT OR REPLACE 幂等 |
bilimuse/db.py:38 |
| ffmpeg 内嵌 |
imageio-ffmpeg 可选 extra,PyPI 拉取不经 GitHub;find_ffmpeg config→PATH→内置 |
bilimuse/services/download.py:32 |
| ffmpeg 路径三级回退 |
config.ffmpeg_path → shutil.which → imageio_ffmpeg.get_ffmpeg_exe()(懒 import) |
download.py:32 |
| mock 未装包 |
setitem(sys.modules,'pkg',None) 让 import 抛 ImportError,模拟未安装 |
tests/test_download.py:44 |
| 网易云无周杰伦版权 |
2018 授权到期,搜索只剩翻唱/remix → 咪咕 MIGUM2.0 为第一源 |
bilimuse/providers/meta.py |
| 咪咕封面 |
用搜索结果 imgItems URL(勿按 id 再搜),webp 用 ffmpeg 管道转 jpeg |
meta.py:96 |
| 标题清洗/匹配 |
clean_title+split_query 拆歌手/歌名,歌手命中±分 |
bilimuse/services/tagger.py:30 |
| B站扫码登录 |
passport qrcode generate/poll;外层 code=0 恒真,看 data.code;成功从 Set-Cookie 取 SESSDATA |
bilimuse/services/auth.py |
| B站搜索双端点 |
登录态优先 wbi,result 空(v_voucher)自动回退旧版 |
bilimuse/providers/bilibili.py:119 |
| 网易云 weapi |
端点用 weapi/search/get/web(cloudsearch 已 50000005);v3 详情用 al/ar 键 |
bilimuse/providers/meta.py:119 |
| 登录降风控 |
匿名流量是 B 站风控重点,SESSDATA 后走 wbi 主链路 |
风险表 |
| fMP4 时长 |
mutagen 读分片 MP4 时长为 0,用 ffmpeg -i -f null - 解析 Duration |
bilimuse/services/aligner.py:38 |
| whisper 模型国内下载 |
hf-mirror 极慢,ModelScope 镜像官方 faster-whisper 快;whisper_model 支持本地路径 |
aligner.py:107 |
| lyric-align 编码 |
Windows GBK 读崩 UTF-8 歌词 → 子进程 PYTHONUTF8=1 |
aligner.py:111 |
| LRC 清理 |
网易云剥作曲/编曲头;LRCLIB 剥空行/live标题行;翻译合并 |
bilimuse/services/lyric.py |
| align 置信度回退 |
直连对齐产出 AlignedLine.to_dict(matched 标记);匹配率<50% 回退原歌词,不盲目缩放 |
aligner.py:_align_in_process |
| 语言自动检测 |
假名→ja 决定性;咪咕 tags 网络信号;whisper_language 兜底。勿硬编码 zh |
aligner.py / providers/meta.py |
| 锚点线性拟合 |
matched<50% 但锚点≥3 时最小二乘 aligned=a·source+b 整体变换(治前留白) |
aligner.py:_apply_alignment |
| 稳健拟合 |
锚点偏移 MAD≤3s 判纯平移(中位数),否则 LSQ;防离群点带偏 |
aligner.py:_robust_fit |
| 外文歌译文 |
主源非netease,detect非zh → 网易云双语对替换(_from_netease_bilingual) |
lyric.py |
| 译文配对 |
校准前按时间戳 pair_translation(英文/拟声行→None),校准后 reattach 按行序贴回;勿行序1:1 |
lyric.py:pair_translation/reattach_translation |
| 歌词正文搜索 |
网易云 weapi s 参数即按歌词搜(返回歌名对、歌手是翻唱者);LRCLIB 不搜歌词正文 |
services/search.py |
| pipeline 事件化 |
编排抽 services/pipeline.py,on_event 推 info/progress/meta/lyric,CLI/TUI/Web 复用 |
pipeline.py |
| Textual 8 |
Input.action_submit() 是协程需 await;headless 用 run_test+pilot.pause() |
tui.py |
「」/《》歌名提取 |
split_query 优先括号内歌名(日式引号),再 - 分隔 |
bilimuse/services/tagger.py |
| cmd 中文不乱码 |
.cmd 存 UTF-8 无 BOM + 首行 @echo off 后 chcp 65001;PowerShell 5.1 则必须 UTF-8 带 BOM,机制相反 |
bilimuse-start.cmd / setup.ps1 |
| 首次配置探测 |
python -c "from bilimuse.config import default_config_dir; sys.exit(...)" + errorlevel,无 venv 无 python 判断直接进安装 |
bilimuse-start.cmd:17 |
| 统一入口 |
启动器管安装/配置/菜单,命令转发 bilimuse.cmd 保持纯净;参数直达 config/tui/web 分流 |
bilimuse-start.cmd |
| 决策桥 |
UI 提供 request(point)->Decision;CLI 内联 input(EOF 兜底),TUI ModalScreen+Future(callback set_result,Esc 兜底) |
bilimuse/tui.py:239 |
| App.query_one 不查模态屏 |
App.query_one 只查基础屏 DOM,弹窗内 widget 用 app.screen.query_one |
tests/test_tui.py:test_manage_modal_save |
| LLM JSON 鲁棒提取 |
response_format=json_object + 平衡括号扫描(容忍 markdown 包裹)+ 失败重试一次 |
services/llm.py:66 |
| 免费预设回落 |
resolve_llm_cfg 空字段回落默认预设(GLM-4-Flash),配置存空即可,模型名变动只改代码 |
services/llm.py:58 |
| 候选化不动 auto |
confirm/ai 才走 fetch_lyrics_candidates(gather 并行+保序+去重+双语),auto 保留早退 |
services/lyric.py:173 |
| Textual 无分隔符 |
OptionList 无 Separator(8.2.8),详情内联进 label,索引 1:1 |
bilimuse/tui.py:32 |
| 去重以文件为准 |
already_downloaded = DB 记录 + file_path 真实存在(文件丢失允许重下);--force 穿透 |
bilimuse/db.py:48 |
| E2E 缓存复用 |
key=bvid:fmt:align 持久 JSON;每用例独立下载目录防同名覆盖;每轮重置 db 抗去重 |
tests/e2e/run_testset.py |
| 标准答案双轨 |
expect 机器校验 + review 人工评审(A/B/C);bvid 实地固化 + 成员集断言抗 B站 排序漂移 |
tests/e2e/cases.yaml |
| pytest 隔离 e2e |
@pytest.mark.e2e + addopts="-m 'not e2e'"(引号防拆分);conftest 显式 fixture 不污染单元 |
pyproject.toml |
| 搜索分区字段 |
B 站搜索返回的是 typeid(子分区 id,193 MV/31 翻唱/130 综合…)非 tid,音乐区判断用 MUSIC_SUB_TIDS 集合 + is_music_tid() |
bilibili.py:_parse_search / search.py |
| Web 静态资源缓存 |
改前端必须 bump index.html 的 ?v=,否则旧 JS + 新 HTML 列错位 |
web/static/index.html |
| WS 评审回传 |
review_result 处理必须 resolve() 返回 Decision 而非 str('str' object has no attribute 'choice') |
web.py:WSReviewBridge |
| Bing 纠错时机 |
编辑距离 ≤1 对中文同样生效(「晴雨」→「晴天」)→ 仅零结果时才替换重搜,建议条始终展示 |
services/suggest.py / search.py |
| MV 字幕零额外 ASR |
MV 音频轨与音频版同流 → 歌词校准(whisper)结果直接 lrc_to_srt 转 .srt |
pipeline.py / db.py:kind |
| 官方原曲终审 |
author/title 含「官方/Official」→ 终审默认推荐原始歌词(MV 前留白/器乐尾会拉偏校准);v1.1 起 auto 模式也跳过 whisper 强制对齐 |
services/aligner.py:is_official_upload |
| WS 发送就绪保护 |
ws.send 未 OPEN 静默丢消息 → 入队 pendingSends,onopen flush |
web/static/app.js |
| 队列竞态 |
create_task 与事件推送顺序不定 → 先发 queued 事件再建任务 |
web.py:DownloadTaskManager |
| whisper 真百分比 |
faster-whisper 无 progress_callback → 惰性生成器 seg.end/info.duration 推 pct;线程内跑(释放 GIL)+ loop.call_soon_threadsafe 桥回事件循环 |
services/aligner.py:104 |
| MCP 帧格式 |
stdio = 换行分隔 JSON(非 Content-Length 头);tools/call 业务失败用 result isError 而非协议 error;unknown -32601 / 坏 JSON -32700 |
bilimuse/mcp_server.py:366 |
| 自定义元素建壳 |
connectedCallback 里 innerHTML=外壳 会摧毁 light-DOM 子节点(弹窗内容丢失)→ 程序化 createElement + 移动子节点 |
web/static/js/components/bm-modal.js:3 |
web -c 保存同路径 |
读用 _load_cfg() 时,写也要 cfg.save(_CONFIG_PATH),否则读自定义写回默认目录 |
bilimuse/web.py:49 |
| 重试 force 判定 |
去重错误真实文案「已下载过且文件存在,跳过」(「去重」只在测试假消息里) |
web/static/js/views/queue-view.js |
| 视图模块装配 |
import 视图模块必须调 init*(store, …),否则模块级 store undefined,store.set 抛 TypeError 静默失败(表单空/引导不开) |
web/static/js/main.js |
| 封面防盗链 |
hdslb 对非 bilibili Referer 返 403 → 图片级 referrerpolicy="no-referrer"(空 pic 先 trim 判空防坏 URL) |
web/static/js/views/search-view.js:renderResults |
| 弹窗 Esc 后被 store 重开 |
modal.hide() 不清 state → 监听 bm-close 事件里清 store.preview,否则任意 store.set 又 show() |
js/views/search-view.js:initSearch |
| 换歌词防陈旧 |
apply 端点重拉 fetch_lyrics_candidates 再按 index 取(弹窗打开期间候选可能变化) |
bilimuse/web.py:api_lyric_apply |
| 歌词预览复用 |
评审 detail 与换歌词 preview 共用 lyric_preview(迁自 pipeline _lyric_preview) |
services/lyric.py |
| 换歌词写盘 |
pair_translation 校准前 / reattach_translation 校准后贴回,占位符不重打标签 |
services/pipeline.py:replace_lyric |
| 前端循环 import |
需要互相调用的视图模块,一个方向改由 main.js 注入(initDownloads(store,{openLyricReplace})),import 单向往 |
js/main.js |
| 队列完成即消失 |
用户裁定:result/cancelled 分支 tasks.delete(id) 立即移除,error 卡保留(重试必需) |
js/main.js:applyEvent |
| whisper 下拉对齐 |
HF 缓存条目 value=size(剥 faster-whisper- 前缀),与 cfg 存储一致;已检测模型去重「按需下载」项 |
js/views/settings-view.js:loadWhisperSelect |
| using 徽章按 kind |
resolve.kind local/cached 限定种类再匹配(本地 value=路径、缓存 value=大小名,互斥防双徽章) |
js/views/models-view.js:refreshModels |
| 测试 mock 要 async |
端点 await 候选函数,monkeypatch 需 async def 包列表,lambda 直接 'list' object can't be awaited |
tests/test_web.py |