BiliTools 机制解析:从 URL 解析到文件落盘的实现路径
【免费下载链接】BiliTools本项目已停止维护。项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools
BiliTools 是一个基于Tauri v2的哔哩哔哩桌面工具箱(当前版本 1.4.7,按 README 约定处于稳定维护期),前端 Vue 3 + TypeScript,后端 Rust,内嵌 aria2c / ffmpeg / DanmakuFactory 三个 sidecar 二进制。读完本文能回答:一次下载请求在前后端之间如何流转、aria2c 侧车的生命周期与断点续传如何实现、WBI 签名与风控回退的代码路径在哪里。
技术底座一览
| 模块 | 选型 | 在链路中的角色 |
|---|---|---|
| 前端 | Vue 3 + TypeScript,src/ | 媒体解析、WBI 签名、风控回退、任务 UI 编排 |
| 后端 | Rust(tauri 2.9.5、tokio、sqlx + SQLite),src-tauri/src/ | 队列状态机、aria2c 管理、sidecar 编排、本地持久化 |
| 下载 | aria2c 侧车(JSON-RPC) | 实际字节下载、限速、会话续传 |
| 后处理 | ffmpeg 侧车 | 音视频合流、mp3/mp4 转码、元数据写入 |
| 弹幕转换 | DanmakuFactory 侧车 | 弹幕 XML → ASS(支持特殊弹幕样式) |
| 持久化 | SQLite(sea-query + sqlx) | 配置、Cookie、任务与调度器状态 |
各平台预编译二进制存放于 src-tauri/binaries/(Windows / macOS / Linux 各架构),默认按可执行文件同目录解析,路径逻辑见 src-tauri/src/storage/config.rs 的SettingsSidecar::get_default。
从 URL 到落盘:主链路走读
入口是搜索页。贴入链接后,整条链路的调用顺序如下:
- 输入识别。parseId 按前缀(
av/BV/ep/ss/md/au/am/cv)与bilibili.com、b23.tv、space.bilibili.com路径模式识别MediaType;空间页 URL 进一步区分 UserVideo / UserOpus / UserAudio / Favorite,b23.tv短链先 302 跟随再递归解析。 - 元信息拉取。getMediaInfo 按 11 种
MediaType分发接口:视频/x/web-interface/view(含分 P、ugc_season 合集、互动视频分支)、番剧/pgc/view/web/season(md前缀会先经/pgc/review/user把media_id换成season_id)、课程/pugv/view/web/season、音乐music-service-c系列、收藏夹先取/x/v3/fav/folder/created/list-all再按 FID 拉资源。 - 播放地址获取。getPlayUrl 按登录态取
qn(登录 127 / 未登录 64)、固定fourk: 1,并按所选格式切fnval:FLV=0、MP4=1、DASH 登录时 4048、未登录 16。响应归一化为三种形态:durls(MP4 多档)、durl(FLV,多qn需并发补拉)、dash(音视频分离)。DASH 模式下音频候选把dash.audio、dash.dolby.audio与dash.flac.audio合并进同一个候选集,杜比全景声与无损 FLAC 就是从这里进入选择的。 - 流协商与回退。handleMedia 用 getDefaultQuality 在候选集里找用户默认档(
res/abr/enc),找不到取最高档;编码档先精确匹配,再按codecid顺序逐个回退。block_pcdn开启时urlFilter按 URL 的os参数把地址分为mirror/upos/bcache三类,剔除 PCDN 节点并替换为官方镜像域名。非 DASH 格式下,audioVideo子任务会被重分类为video(单文件流不需要合流)。 - 提交与调度。submit 按勾选项经
selectToSubTasks生成子任务列表(Video、Audio、AudioVideo、Thumb、HistoryDanmaku、Subtitles、AISummary、NFO 等),调backend.commands.submitTask落库并进 Backlog 队列。auto_download开启时processQueue继续调planScheduler(一次性清空 Backlog,建 Scheduler 进 Pending)与processScheduler。后端 Scheduler::dispatch 用RUNTIME.semaphore(max_conc个许可,默认 3)控并发,任务完成后 Scheduler 移入 Complete 并发系统通知。 - 任务执行。Task::process 为任务建临时目录
{temp_dir}/{task_id},handle_task 先向 Vue 请求PrepareTask(回传videoUrls/audioUrls/subtasks/subFolder)与每个子任务的GetFilename,再按类型路由:Video/Audio走 aria2 下载 + 后处理;AudioVideo从共享的OnceCell取两条流路径做 ffmpeg 合流;其余子任务向前端要内容后写文件。每个子任务都包在 try_join 里,实现取消 / 暂停 / 重试。 - 下载与落盘。aria2c::download 以 subtask id 作为 aria2 gid,
addUri指定临时目录与文件名,随后每秒tellStatus轮询,经subtask.send(content, chunk)推进度到前端并写库;完成后post_media依abr决定扩展名(30250 →eac3,30251/30252 →flac,其余m4a;合流档对应mkv/mp4),按需走convert_mp3/convert_mp4/add_meta,最后fs::copy进目标文件夹,get_unique_path 处理重名。弹幕、NFO、SRT、AI 摘要等文件也落同一文件夹。
前后端之间的"反向调用"值得单独说明。frontend::request 的工作方式:Rust 侧开一个 oneshot channel、用app.once(endpoint, ...)注册一次性监听,emitQueueEvent::Request后挂起等待;Vue 侧 handleEvent 收到request事件后按RequestAction分发到getPlayUrl、getDanmaku、getSubtitle等实现,结果经app.emit(endpoint, result)回传。所有 B 站 API 调用、弹幕 XML 拼装、SRT 重排、NFO 生成都走这条通道,而不是 Rust 直接发 HTTP。原因见下一节。
关键实现决策
为什么解析与签名留在 webview,Rust 只管进程和 IO
Rust 侧完全可以用 reqwest 复刻全部 B 站接口,但有三件事只能在 webview 里做:WBI 签名需要 canvas 的 WebGL 指纹(getWebGLFingerPrint);风控回退需要 Geetest 在 DOM 里交互(window.initGeetest);弹幕解析与 NFO 生成依赖DOMParser/XMLSerializer。反过来,让 Rust 承担 HTTP 栈就要再实现一遍风控回退逻辑。所以划分是:webview 当"解析与签名执行器",Rust 当"队列状态机 + 进程编排器",frontend::request事件通道就是两者之间的 RPC。代价是调试链路变长——一次下载请求要跨进程往返两次(PrepareTask与GetFilename)。
aria2c 侧车:生命周期、断点续传与孤儿进程
启动时 aria2c::init 用TcpListener::bind(127.0.0.1: 0)抢一个空闲端口,生成 8 位随机rpc-secret,以 sidecar 方式拉起进程。固定参数包括:--referer=https://www.bilibili.com/与Origin头(规避防盗链)、--user-agent写死为桌面 Chrome UA、--max-download-limit={speed_limit}K、--pause=true(所有任务默认暂停态,由handle_media的CtrlEvent::Resume分支逐个unpause)、--input-file/--save-session指向数据目录下aria2.session且--save-session-interval=5。
续传机制的关键是gid 稳定:gid 直接取 subtask id(前端randomString(8)生成,入库后不再变)。任务重跑(retry)时先tellStatus,命中complete直接跳过、paused直接unpause、任务不存在才addUri。轮询循环里错误码 31(connection refused)被显式忽略等待重试。配合每 5 秒落盘的会话文件,应用重启后未完成任务可以基于同一会话恢复。
其他细节:RPC 客户端单独no_proxy(用户代理不应作用于本机回环)、3 秒超时、HTTP/1 only;daemon协程每 3 秒getGlobalStat保活并监视 stdout/stderr,进程意外退出会经process_err上报前端;Windows 上额外起一个 PowerShell 轮询宿主 PID,宿主先退出时强杀 aria2c,防止孤儿进程。取消子任务时依次purgeDownloadResult/removeDownloadResult/forceRemove;所有"取消时该杀什么"统一注册为CtrlHandle上的 cleaner(杀子进程、删临时目录、清 aria2 任务),取消与 retry 时由clean_all批量执行。
WBI 签名、风控回退与 Cookie 生命周期
签名实现在 wbi():每次请求都从/x/web-interface/nav取wbi_img的 img / sub key,经固定 64 位置换表混出 32 位mixinKey;query 按键名排序、值过滤[!'()*]后拼接,w_rid = md5(query + mixinKey)。签名同时注入wts与 WebGL 指纹字段dm_img_str/dm_cover_img_str(canvas WebGL version 与 unmasked renderer 的 base64 截断)。注意这意味着每次 WBI 请求都会附带一次nav调用。
风控回退集中在 tryFetch:响应code === -352时取data.v_voucher,连同 Cookie 里的bili_jct作 csrf 调/x/gaia-vgate/v1/register拿 geetest 参数,弹 Geetest 验证后经/x/gaia-vgate/v1/validate取得grisk_id,随机延迟 100~500ms 后带gaia_vtoken参数重试,默认最多 3 轮。弹幕分段拉取里也有同样的 100~500ms 随机间隔。
票据与登录态在后端 login.rs:get_bili_ticket用 HMAC-SHA256(keyXgwSnGZ1p,消息ts{ts})生成hexsign,调GenWebTicket换取bili_ticketcookie;扫码登录后端以 1 秒间隔轮询qrcode/poll,经 TauriChannel把 code 推给前端,code 0时把Set-Cookie与refresh_token写入cookies表并HEADERS.refresh()刷新全局请求头;refresh_cookie是两步提交——cookie/refresh换发后再confirm/refresh确认。前端checkRefresh在每次拉用户信息前检查/x/passport-login/web/cookie/info的refresh标志,需要刷新时先从/correspond/1/{path}页面提取refresh_csrf(refresh_{timestamp}经 RSA-OAEP 加密得到路径段)。Cookie 统一存cookies表(name 主键,insert解析path/domain/expires/httponly/secure),内存中由HEADERS静态结构维护并同步给前端。
配置与数据足迹
数据根目录(README 与代码一致,WORKING_PATH = app_data_dir()):
| 平台 | 路径 |
|---|---|
| Windows | %AppData%\com.btjawa.bilitools |
| macOS | $HOME/Library/Application Support/com.btjawa.bilitools |
| Linux | $HOME/.local/share/com.btjawa.bilitools |
该目录下的Storage子目录存放 SQLite 库,含config、cookies、tasks、schedulers、queue五张表(建表语句见 src-tauri/src/storage/);aria2.session与DanmakuFactory.json也在数据目录,aria2 日志在平台 log 目录。任务临时文件位于temp_dir/com.btjawa.bilitools/{task_id},任务结束即删。
主要配置项(默认值定义于 src-tauri/src/shared.rs 的CONFIG,运行时存config表并按 key 覆盖默认):
max_conc:并发任务数,默认 3;写入时经 RUNTIME.new_conc 即时调整信号量down_dir:下载根目录,默认桌面speed_limit:aria2 限速(KB/s),默认 0 不限;启动时写入--max-download-limit,改动后需重启生效block_pcdn:默认 true,PCDN 地址过滤add_metadata:默认 true,ffmpeg 写入标题 / 简介 / 封面 /original_url等convert.danmaku/convert.mp3/convert.mp4:默认 true / false / falsedefault.res/abr/enc:默认 80 / 30280 / 7auto_download:默认 false,提交任务后不自动开跑organize.auto_rename:默认 true,重名文件自动加_1、_2序号format.series/item/file:三级命名模板,支持downtime:YYYY-MM-DD_HH-mm-ss这类 strftime 后缀
边界
项目以 GPL-3.0-or-later 开源:再分发必须保持开源、沿用相同许可证并保留全部原始版权信息(见 LICENSE 与 README_EN.md)。
- 不绕过鉴权、不破解付费资源;VIP / 付费内容仅对应已订阅账号
- 与 Bilibili 无隶属、合作、授权或背书关系,"哔哩哔哩"商标归上海幻电信息科技
- 获取内容的版权归原权利方;项目定位为学习与测试用途,使用后果由用户自行承担
设计取向
BiliTools 把易碎的部分放在 webview、把稳定的部分放在 Rust:解析与签名依赖 DOM 和风控环境,留在 Vue 侧;进程编排、SQLite 状态机与 sidecar 生命周期下沉 Rust,且每处状态变更都是"内存 + 前端事件 + 数据库"三写,换来崩溃或重启后队列可恢复(interrupted判定 +restore重新 dispatch)。延伸阅读见 README_EN.md 与 docs/guide/ 下的安装、解析、登录等章节。
【免费下载链接】BiliTools本项目已停止维护。项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考