Skip to content

About

lanyeeee/bilibili-video-downloader 插件:下载时自动把集数补零 第1话→第01话,位数按合集集数自动推导 | Bilibili video downloader plugin: auto pad episode numbers

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

bdp-autopad · 自动补零插件

给 哔哩哔哩视频下载器(lanyeeee/bilibili-video-downloader)写的插件:下载时自动把集数补零。

  • 第1话 羽丘的不可思议女孩 → 第01话 羽丘的不可思议女孩
  • 目录格式只写 {episode_order} 时的裸数字 1 → 01
  • 补几位不用你填——插件自己从下载任务里推断合集集数

⚠️ 关于本仓库 代码与文档由 AI 撰写,但已经过实测可用。下面把验证项目逐项列出, 而不是笼统写一句「测过」—— 你可以照着命令自己复核任何一项:

验证项 v0.1.2(上一发布版) 本仓库 v1.0.0
单元 / 集成测试 36 项 74 项
补零行为等价性 人工对照 336×4 = 1344 组 自动差分 138,618 次逐字节比对
端到端 hook(P/Invoke,含真实数据) 3 组 55 项 9 组 186 项
宿主加载序列仿真 无 18 项(复刻 load_plugin_from_path / execute_hook / apply_output)
导出 ABI 对照 — 5 个符号与上一版逐字一致
静态检查 未配置 clippy 零告警(panic 类 lint 为 deny)
在下载器 GUI 内加载 ✅ 已在 bilibili-video-downloader v0.2.1 中加载成功 ✅ v0.2.1 实测加载成功(宿主日志原文见下)
在 GUI 内触发真实下载(AfterPrepare 被调用) ✅ ⏳ 需要一次真实下载才能触发(见下)

已在真实宿主里跑通加载。把 dist\bdp_autopad.dll 装进插件目录后启动 bilibili-video-downloader v0.2.1,宿主自己写下:

2026-10-20T11:03:38.270 INFO 插件加载成功:
  plugin_name=自动补零 第01话 (bdp-autopad, v1.0.0),
  plugin_path=C:\Users\Admin\Videos\bilibili-plugin\bdp_autopad.dll
  target=bilibili_video_downloader_lib::plugin::plugin_manager (plugin_manager.rs:60)

全程无 error / warn;宿主随后把 plugin.json 的 descriptor 更新为 version: 1.0.0。 除加载外,前六项验证也都做到了,且比上一版严格得多 —— 尤其「宿主加载序列仿真」逐句复刻了 宿主的 plugin_loader.rs / plugin_executor.rs / hook_context.rs,包含上一版测试没覆盖的 set_host_api rc 校验与 task_id 不被改动的校验。

唯一还没做的就是「在 GUI 里真的下载一集」(AfterPrepare 只在 prepare 成功后才触发, 需要一次真实下载)。做法:在下载器里粘任意一个 B 站视频链接开始下载,第一集 prepare 完就会走到插件; 装好后的目录/文件名会直接体现补零结果。

v0.1.3 是行为等价重构(内部结构、性能、健壮性全面提升,外部行为逐字节不变); v1.0.0 在 v0.1.2 的基础上完成重构与验证补强,并修掉代码审查发现的 9 处缺陷, 详见下方更新记录 与怎么证明没有改坏。

不用编译,直接用:dist/bdp_autopad.dll 就是编译好的成品, 也可以在 Releases 里直接下载。


为什么必须写插件

下载器自带的目录格式模板做不到这件事。它用的 strfmt 格式化库明确不支持零填充:

{episode_order:03}  →  error: sign aware zero padding and Align '=' not yet supported

而 {episode_title} 只是普通字符串,模板引擎不会去解析里面的 第1话 再补零。所以在模板层是无解的。

好在宿主暴露了 AfterPrepare 钩子,会在创建目录之前把 episode_dir / filename 交给插件改写——本插件就是在这里下手。


安装(3 步)

  1. 把 dist/bdp_autopad.dll 放到一个不会被清理的固定目录,例如 C:\Users\<你>\Videos\bilibili-plugin\bdp_autopad.dll (后端要求绝对路径且文件必须存在,别放临时目录或下载文件夹)

  2. 打开下载器 → 设置 → 插件面板 → 点 「添加插件」 → 选中该 dll

  3. 确认状态显示 「已加载」,然后开始下载

插件出错时按 FailOpen 记录日志并继续,不会中断你的下载任务。


补零规则

位数由合集集数自动决定

插件读取 %APPDATA%\com.lanyeeee.bilibili-video-downloader\.下载任务\*.json,按 collection_title 聚合出 episode_order 的最大值:

合集集数 输出宽度 例
≤ 99 2 位 第01话
≤ 999 3 位 第001话
≤ 9999 4 位 第0001话

支持的写法

输入 12 集合集的输出
第1话 羽丘的不可思议女孩 第01话 羽丘的不可思议女孩
第12话 风吹浪打,亦不沉没 第12话 风吹浪打,亦不沉没(够长就不动)
第1話 羽丘的不可思议女孩(U+8A71) 第01話 羽丘的不可思议女孩
第1巻 序章(U+5D29) 第01巻 序章
EP1 标题 / Ep 1 / ep.1 EP01 标题 / Ep 01 / ep.01
[1] 标题 / 【2】标题 [01] 标题 / 【02】标题
1(纯数字) 01
2. 02.
1 - 标题 01 - 标题
1 第100话 x 01 第100话 x(前导数字照常补,右侧够长不动)
第1话 标题-P2 分P名 第01话 标题-P02 分P名
1080p / 128kbps / 2024 原样不动
正片 / 原版 / 第1.5话 特别篇 原样不动
OAD 感冒综合征 / 特别篇 拈花夜话 原样不动(无集数就不动)
DEEP1 / STEP3 / HDD2 原样不动(EP 守卫)
[1080p] / [AB12] 原样不动(方括号里不是集数)
-P1080 / -P1080p / -p2 原样不动(分P 守卫)
  • 主路径:第N话 / 第N集 / 第N期 / 第N話 / 第N巻
  • 也支持 EP1、Ep 1、ep.1 前缀写法
  • 也支持行首方括号编号 [1]、【2】
  • 也支持纯数字开头的文件名(1、2.、1 - 标题)
  • 也支持分P 序号后缀(-P2 → -P02)
  • 以上规则可叠加:一个名字里出现多种写法时各补各的; 只有当所有规则都没产生实际改动时,才退回「开头纯数字」兜底
  • .mp4 扩展名始终原样保留
  • 只补位、不截断,补过零的不会叠加

regex crate 不支持环视,以上所有"不该动"的边界判断都是用代码手工做的(取匹配后看前后字符), 所以每条守卫都有对应测试覆盖,见下方 tools/smoke-test.ps1 与差分测试。


配置

首次加载会在下载器数据目录自动生成:

%APPDATA%\com.lanyeeee.bilibili-video-downloader\autopad.toml
enabled = true        # 总开关
min_width = 2         # 宽度下限(2 = 12 集的季番也写成 第01话)
max_width = 4         # 宽度上限
fixed_width = false   # true = 忽略自动探测,一律用 min_width
pad_filename = true   # 文件名里的集数是否补零
pad_episode_dir = true
verbose = false       # true = 往控制台打探测日志

改完需要重启下载器(配置只在进程启动后读一次)。 也可以用环境变量临时覆盖:AUTOPAD_MIN_WIDTH=3、AUTOPAD_ENABLED=0。

配置文本读取有 64 KiB 上限,超限视同读失败并回落默认值(不截断解析)。


影响范围

维度 情况
运行位置 只在下载器进程内,靠 AfterPrepare 钩子调用
改什么 只改本次任务的 episode_dir / filename
老文件 不会动已经下载好的文件(钩子只在下载流程中触发)
其他软件 完全不碰,不注册全局钩子、不装服务、不开机自启

权限

先说清楚:宿主的插件系统没有任何沙箱或权限约束——插件是进程内动态库,与下载器同权限运行,官方 README 也明确警告过这一点。

所以下面这些不是系统强制的最小权限,而是本插件代码自身的实际接触面:

  • 只声明了 3 个钩子中的 1 个(AfterPrepare),BeforeVideoProcess / OnCompleted 完全不参与
  • 从未调用宿主提供的唯一 Host API(host::get_config()),因此接触不到 sessdata 等敏感配置
  • 静态导入表里没有网络组件——没有 ws2_32(socket)、winhttp、wininet;也没有 advapi32(注册表)、shell32(COM/shell),只有 KERNEL32 / ntdll / UCRT 系列 / bcryptprimitives
  • 文件系统只碰三处:读 .下载任务\*.json、读 autopad.toml、仅当配置不存在时写一份默认 autopad.toml
  • 环境变量只读 APPDATA / HOME,以及可选的 AUTOPAD_MIN_WIDTH / AUTOPAD_ENABLED
  • 代码零 unsafe,且用 lint 把「不许新增 unsafe」固化(见下)

自查导入表:

objdump -p dist/bdp_autopad.dll | findstr "DLL Name"

诚实补充:没有沙箱意味着技术上插件可以调用更多 API(例如动态 LoadLibrary),上表只说明它现在没有。要长期安心,请自行审阅 src/ —— 一共 6 个生产文件、约 1650 行(不含测试代码)。


自己编译

工具链:Rust (GNU target) + mingw-w64(提供 gcc / as / dlltool,缺了会在 parking_lot_core 处报 dlltool could not create import library)。

cd bdp-autopad
cargo test --release     # 74 个单元/集成测试
cargo build --release    # 产物 target\release\bdp_autopad.dll

⚠️ 本机若同时装了 scoop 版(MSVC)与 rustup 版(GNU)两套 Rust,cargo 会按 PATH 顺序取到其中一个。必须用 GNU 那条,否则编出来的 dll 会链接到 MSVC 运行时,与仓库文档和既有成品不一致。确认方式: cargo -vV 的第一行 host: 应是 x86_64-pc-windows-gnu; 不是的话把 %USERPROFILE%\.cargo\bin 放到 PATH 最前面再执行。

端到端冒烟测试(不用开下载器 GUI)

cargo test 只能证明补零函数写对了,证明不了宿主真的会这么调用。 tools\smoke-test.ps1 用 P/Invoke 直接加载 dll,按宿主 v0.2.1 真实的 extern "C" 符号 + HookInputV1 JSON 协议喂输入,逐项核对输出。

v0.1.3 起脚本自带合成数据集(临时 APPDATA + 12/100/1000 集三个合集), 不再依赖本机是否下载过某个番剧,结果完全确定:

# 必须 -ExecutionPolicy Bypass(系统默认禁止运行脚本)
powershell -NoProfile -ExecutionPolicy Bypass -File tools\smoke-test.ps1
# 或直接测刚编出来的:
powershell -NoProfile -ExecutionPolicy Bypass -File tools\smoke-test.ps1 -DllPath target\release\bdp_autopad.dll

9 个断言组,覆盖配置面的每一种组合(每个 -Expect 必须单独开进程,因为配置是进程级单例):

-Expect 验证什么 断言数
default(不传) 全套补零行为 + 宽度推导(2/3/4 位)+ 默认配置自动生成 49
NoPad 设 AUTOPAD_ENABLED=0 后必须一个都不改 32
Disabled 配置文件 enabled = false,同样一个都不改 32
BadConfig 配置文件塞满非法值 / 未知键 / 全角等号:必须回落默认值、照常工作、不覆写用户文件 11
Width3 AUTOPAD_MIN_WIDTH=3 后宽度必须变 3 18
FixedWidth fixed_width = true + min_width = 3,忽略探测 18
FileOnly pad_episode_dir = false,只改文件名 8
DirOnly pad_filename = false,只改目录(且父级不动) 8
RealData 对本机真实 .下载任务 数据只读抽查 9
powershell -NoProfile -ExecutionPolicy Bypass -File tools\smoke-test.ps1 -Expect Width3
powershell -NoProfile -ExecutionPolicy Bypass -File tools\smoke-test.ps1 -Expect RealData

只读,不碰任何下载文件与 autopad.toml;临时目录用完即删。

脚本文件头必须是 UTF-8 带 BOM,否则 Windows PowerShell 5.1 会按 GBK 解码, 中文字符串全乱(这是踩过的坑之一)。

宿主加载序列仿真(验证「宿主装得起来、调得起来」)

冒烟测试只证明补零行为对;这个脚本证明宿主能加载它并按协议调用。 它逐句复刻宿主源码 src-tauri/src/plugin/ 下的三条路径,而不是凭协议猜:

阶段 复刻的宿主代码 检查点
加载 plugin_loader.rs :: load_plugin_from_path 绝对路径 / 文件存在 / dlopen 全部 5 个导出符号 / sdk_api_version==1 / id 非空 / hooks 非空 / set_host_api_v1 返回 rc=0
调用 plugin_executor.rs :: execute_hook、call_on_hook_blocking on_hook_v1 rc=0、输出缓冲非空、free_buffer 正常
应用 hook_context.rs :: apply_output payload 变体与 hook_point 匹配、task_id 未被插件改动
探针 — 注入带计数的 get_config_json 回调,断言插件一次都没调用宿主 Host API
失败路径 plugin_manager.rs :: run_hook 的 FailOpen 分支 输入畸形时 rc≠0 且 last_error 可读
  & .\tools\host-load-test.ps1
  & .\tools\host-load-test.ps1 -DllPath <别的 dll 做对照>

18 项断言。实测 v1.0.0 与本仓库里的 v0.1.2 成品 dll 结果逐项一致。

其中 set_host_api rc 校验是 v0.1.2 时期的测试没有覆盖的一步 —— 宿主在这里 rc≠0 会直接拒绝加载,补上这步才算真正对齐 load_plugin_from_path。

性能基准

# 默认测 dist\bdp_autopad.dll;加 -DllPath 可测别的(如旧版本做对照)
powershell -NoProfile -ExecutionPolicy Bypass -File tools\bench-scan.ps1

造 400 个 ~14 KB 的任务文件(贴近真实规模),分别测「冷启动 / 目录未变 / 新增 1 个文件」三档耗时, 并抽查结果正确性。当前实测(每档 5 轮取中位):

场景 v0.1.2 v0.1.3 说明
冷启动(首次 hook) 260 ms 253 ms 等价,都被文件读取与杀软扫描主导
目录未变 0.61 ms 0.65 ms 等价,指纹命中、零文件内容读取
新增 1 个任务文件 78.8 ms 1.95 ms 约 40 倍 —— 批量下载的稳态路径

工程结构

├── Cargo.toml            # cdylib;release 关掉了 panic=abort(见下);生产代码禁 unwrap/panic
├── src/
│   ├── lib.rs            # 插件协议编排:descriptor + on_hook + handle_hook(策略本体)
│   ├── pad.rs            # 补零规则(纯函数,零 IO)+ 单元测试
│   ├── config.rs         # autopad.toml 解析(零依赖 key=value)+ 环境变量覆盖
│   ├── episode_scan.rs   # 扫 .下载任务/*.json 聚合集数(目录指纹 + 逐文件增量缓存)
│   ├── paths.rs          # 宿主数据目录定位
│   ├── legacy_reference.rs  # 【仅测试】v0.1.2 补零逻辑逐字副本,差分对照用
│   └── equiv_tests.rs       # 【仅测试】差分测试(新旧实现逐字节对照)
├── tests/data/episode_titles.txt  # 336 条真实 episode_title 语料(差分测试输入)
├── tools/
│   ├── smoke-test.ps1     # P/Invoke 端到端冒烟测试(9 组 186 项断言)
│   ├── host-load-test.ps1 # 宿主加载序列仿真(18 项,复刻 plugin_loader/executor)
│   └── bench-scan.ps1     # 集数探测性能基准
├── vendor/               # 宿主 v0.2.1 的 plugin-api / plugin-sdk(MIT)
└── dist/bdp_autopad.dll  # 编译产物

模块划分不只是"好看":补零规则必须可被穷举验证,所以它被隔离成零 IO 的纯函数 (pad.rs 不知道磁盘、配置、锁的存在);所有可能失败的 IO 都集中在 config / episode_scan, 失败时一律回落安全默认值。

怎么证明没有改坏

重构的头号约束是「不得造成任何功能缺失或行为改变」。口头保证没有价值,所以给了三层证据:

  1. 差分测试(cargo test,最强的一层) src/legacy_reference.rs 里放着 v0.1.2 补零逻辑的逐字副本(连 \d 的 Unicode 语义都保留), 与 pad.rs 在同一个测试进程里对同一批输入跑两遍、逐字节比对:

    语料 来源 条数 比对次数
    真实标题 本机 415 个任务文件里的 336 条 episode_title 336 9072
    原测试用例 v0.1.2 单元测试里手写过的全部用例 91 2457
    边界输入 空串、控制字符、全角/阿拉伯数字、超长数字、1 万字符长串…… 99 2673
    片段组合 24 种格式片段两两/三三拼接的组合爆炸 4608 124416
    合计 5134 138618

    每条输入都按 9 种宽度(0/1/2/3/4/5/6/16/64)× 3 种形态(纯文件名、裸路径、完整路径)比对。

  2. 五条规则各自的见证用例 —— 差分测试只能证明「新旧一致」,证明不了「规则还在工作」 (若某条规则被整体删掉且语料恰好不覆盖,差分同样会通过)。因此 all_five_rules_are_individually_exercised 为每条规则钉了一个「只有它才会产生该输出」的输入。

  3. 同一套冒烟断言跑两个 dll —— tools\smoke-test.ps1 的 9 组 186 项断言 分别对 v0.1.2 与 v1.0.0 的成品 dll 各跑一遍,结果完全一致(9/9 组、0 失败)。

  4. 宿主加载序列仿真跑两个 dll —— tools\host-load-test.ps1 的 18 项断言 同样对新旧 dll 各跑一遍,逐项一致;导出 ABI(5 个符号)也逐字一致。

  5. 原项目自带的验收脚本原样复跑 —— 未改动的 tools\smoke-test.ps1(v0.1.2 版, 3 组 55 项)直接指向新 dll 运行,55/55 全通过,与指向 v0.1.2 成品 dll 的结果相同。

踩过的坑(改代码前必读)

  1. regex crate 不支持环视((?=...)、(?!...)),写进去会直接编译失败。 「数字后面不能是字母」这条规则只能取出来用代码判断。
  2. Cargo.toml 里绝不能写 panic = "abort"。SDK 的 export_plugin_v1! 靠 catch_unwind 把插件异常转成错误码,配合 FailOpen 让宿主记日志继续跑; 改成 abort 会让插件里任何 panic 直接崩掉整个下载器进程。
  3. 不能 use std::sync::LazyLock——export_plugin_v1! 宏展开时也导入了它, 同作用域重复导入报 E0252,改用全路径引用。
  4. 宿主只校验 task_id 不可改;episode_dir / filename 可自由修改, 而且 create_dir_all 在 hook 返回之后才执行,所以改动会真正生效。
  5. 写 plugin.json 必须用无 BOM 的 UTF-8——宿主是 serde_json::from_str(...).unwrap_or_default(),带 BOM 会静默解析失败变成空表, 插件看着写进去了却根本不加载。
  6. 别用 LazyLock 承载可能 panic 的初始化。LazyLock 的初始化体一旦 panic, 该实例就永久中毒:之后每次访问都重新 panic,且初始化体不会再被调用, 没有任何自愈途径。Config::get() 原来正是这么写的,而 Config::load() 里的 eprintln! 在 stderr 不可写时(例如输出端管道已关闭)会 panic —— 结果是插件在该进程内彻底失效且永不恢复。 现改为 OnceLock(初始化失败可重试)+ 吞掉写错误的 log_err()。
  7. 日志不要用 eprintln!,理由同上:插件里的日志失败绝不该升级成功能失败。 统一走 config::log_err(),它丢弃所有写入错误。
  8. 宽度必须夹到 1..=64。宽度会进 format!("{:0>width$}"),Rust 的格式化填充 宽度有内部上限,usize::MAX 会直接 panic。pad.rs 的 MAX_WIDTH 是单一事实来源, Config::normalize 与 pad_text 入口各夹一次。
  9. Take<T> 不实现 size_hint。用 file.take(max) 做读取上限后, 若不给 String::with_capacity,一个 14 KB 的文件要从 32 字节反复倍增约 10 次 (实测冷启动因此慢约 35%)。用已打开句柄的 metadata() 定容即可—— 这不是多一次打开,std::fs::read_to_string 内部本来也这么做。
  10. regex 的 \d 是 Unicode 语义,能匹配 ٣、1 这类字符,但它们永远无法被 str::parse::<u32>() 解析(Rust 整数解析只认 ASCII),所以命中后也只会原样返回。 收窄成 [0-9] 输出逐字节不变,还能顺带做「没有 ASCII 数字就直接短路」—— v0.1.3 正是这么改的,差分测试覆盖了这批输入。

更新记录

v1.0.0 —— 正式版

与 v0.1.2(上一发布版)相比,对外行为逐字节不变。这一版做的是内部重构与验证补强, 不是功能升级:没有新增补零格式、没有新增配置项、没有改变任何输出。

方向 变化
性能 批量下载时单个任务的集数探测 55.9 ms → 1.49 ms(约 37×)(400 文件数据集,9 轮取中位)
可验证性 单元/集成测试 36 → 74;端到端冒烟 3 组 55 项 → 9 组 186 项;新增 138,618 次自动差分比对与 18 项宿主加载序列仿真
可维护性 单个 720 行 lib.rs 拆成 6 个模块;补零规则独立为零 IO 纯函数,可被穷举验证
健壮性 配置 64 KiB / 任务文件 8 MiB 读取上限(超限明确失败,不截断解析);逐文件缓存 20,000 条上限;unwrap/expect/panic 在 Cargo.toml 里设为 deny
可诊断性 默认配置写入失败、配置项非法值不再静默,改为打日志
实测 已在 bilibili-video-downloader v0.2.1 中加载成功(宿主 plugin.json 与日志为证)

版本号从 0.1.x 跳到 1.0.0 是稳定性声明:行为已在真实下载数据上验证 (本机 354 条可比对条目中,需要补零的 164 条 100% 正确),接口与配置格式就此冻结。

升级方式不变:替换 dll 即可,无需改任何设置。

v0.1.4 —— 修复代码审查确认的 9 处缺陷

审查方式:逐文件人工审查 + 用探针工程把每个可疑点写成可执行断言,先复现、再修。 只列实际改动,行为兼容性单独说明。

# 缺陷 触发条件 后果 修法
B1 超尺寸文件被当成「读失败」 .下载任务 里存在 > 8 MiB 的 .json stamp_after 把目录指纹置为永久无效,之后每一次 hook 都放弃「目录未变 → 零成本」的指纹短路、重建整张映射(实测 0.65 ms → 1.95 ms,约 3 倍),且用户无从知晓 新增 ParseOutcome::Skipped:尺寸超限是确定性失败,照常写缓存、不计入 read_failed、不让指纹失效;首次出现时打一条日志提醒
B2 基准脚本把 APPDATA 还原成 %USERPROFILE% tools\bench-scan.ps1 每次运行 环境变量被改成错误的值(脚本进程结束即退,无实际后果;但同进程后续逻辑会读错目录) 开头留底 $realAppData,结束原样还原
B3 两个测试脚本无异常清理 脚本中途 throw(例如 hook 返回非 0) 临时目录残留、APPDATA 停留在沙箱路径 冒烟脚本加 trap + Clear-Sandbox;基准脚本加 try/finally
B4 默认配置写入失败被静默吞掉 宿主数据目录不存在 / 无写权限 用户按文档去目录里找不到 autopad.toml,且没有任何提示可定位 写失败时打日志(附路径与错误)
B5 配置文件里的非法值静默忽略 enabled = ture、min_width = 三个 等 配置「看起来写进去了、实际完全没生效」;而同样非法的值走环境变量却会打日志,两条路径行为不一致 抽出 set_bool / set_usize,无法识别时保留原值并打日志
B6 README 声称 v0.1.3 已在宿主实测 — 文档与实际验证范围不符(v0.1.1/v0.1.2 确实在 v0.2.1 里跑过,v0.1.3 没有) 改成按版本分列的验证程度表,明确标出「未在 GUI 内跑过真实下载」
B7 扫描状态非原子提交(预防性) 将来在 build_map 与 refresh 之间引入任何 panic 源 会留下「缓存是新的、指纹还是旧的」的不一致状态,下次刷新直接跳过重扫并沿用过期映射 → 补零位数静默算小 build_map 改为只读缓存、返回新缓存;refresh 拿到返回值后四个字段连续一次性提交
B9 逐文件缓存无容量上限 .下载任务 长期累积(宿主不自动清理) 常驻内存随目录文件数线性增长 新增 MAX_CACHE_ENTRIES = 20_000,超过则整体停用缓存(退回全量扫描语义,结果不变,内存封顶)
B12 config 与 episode_scan 各有一份限额读取实现 — 重复代码;更关键的是「尺寸超限」没有独立类型,B1 因此无从区分 抽成 src/text_file.rs,ReadTextError::TooLarge 让「确定性失败」与「可能自愈的失败」在类型层面分开

兼容性:功能行为完全不变(差分测试 13.8 万次比对仍全一致,9 组冒烟断言全通过)。 唯一新增的可观测输出是日志(更啰嗦了一点,仅在有异常输入时出现),以及 verbose 日志多两个字段。

「修复前必然失败」的证据:B1 / B9 的回归测试在回退修复的副本上实测失败 (regression_b1_oversized_file_does_not_invalidate_stamp、regression_b1_stable_state_survives_oversized_file、 regression_b9_cache_is_capped 三项),加回修复后通过。B7 是预防性改动,无法用运行时断言复现(当前代码里没有 panic 源), 其契约由 regression_b7_build_map_does_not_mutate_caller_cache 钉住。

审查中确认「不是 bug」的项(已记录,避免后人重复怀疑):

  • pad_path 对 C:\dl\1\(尾部分隔符)、C:(裸盘符)、\\、UNC 路径的处理均正确;尾部 \ 丢失不影响语义。
  • extract_json_string / extract_order 对本机 415 个真实任务文件的抽取结果与完整 JSON 解析逐条一致(0 处差异),且两个 key 在每个文件里都只出现一次。
  • list_json_files 的指纹依赖 read_dir 顺序,顺序变化只会触发一次多余重建,不影响结果。
  • json_unescape 的全部切片都有长度前置检查,无越界风险。
  • 方括号规则只认行首编号([字幕组][1] 不补)是 v0.1.0 起的既有设计,非回归。
  • 宽度按「已出现的最大集数」推导,因此同一合集补下更高编号的集数后位数可能变大(第01话 vs 第100话)——同理是既有设计。

v0.1.3 —— 行为等价重构(性能 / 健壮性 / 可维护性)

外部行为逐字节不变(见怎么证明没有改坏),改动全在内部:

方向 改动 效果
性能 集数探测改为逐文件增量缓存:指纹变化时不再整目录重读,只读新增/改动的文件 批量下载稳态单次刷新 78.8 ms → 1.95 ms(约 40×)
性能 补零各规则改为「零改动不分配」(返回 Option<String>,仅在确有改动时构造新串) 一个普通文件名从 5~6 次堆分配降到 1 次
性能 无 ASCII 数字时直接短路,跳过 5 趟正则;merge_max 不再为每个文件重复分配键 见基准
稳定 正则改 OnceLock<Option<Regex>>,编译失败视为「永不匹配」——既不会 LazyLock 中毒,也不会把 panic 抛过 extern "C" 边界 消除一类「进程内永久失效」
稳定 宽度硬上限收敛到 pad::MAX_WIDTH 单一常量,pad_text / pad_path 入口各夹一次 绕过配置也不会 panic
安全 配置文件与任务文件读取加字节上限(64 KiB / 8 MiB),超限不截断而是按读失败处理 异常文件撑不爆内存,也不会抽出错位的标题
安全 Cargo.toml 里把 unwrap / expect / panic / todo / dbg / print_std* 全部设为 deny(仅 cfg(test) 局部放开) 生产代码里 panic 变成编译错误
可维护 拆成 5 个生产模块(pad / config / episode_scan / paths / lib),pad 是零 IO 纯函数 补零规则可被穷举验证
可维护 handle_hook(cfg, input) 与 on_hook 解耦:策略本体不再依赖进程级单例配置 钩子策略可确定性测试(新增 9 项)
可维护 新增差分测试框架(含 336 条真实语料),测试从 36 项增至 66 项 行为等价有硬证据
可维护 冒烟测试改为自带合成数据集,不再依赖本机下载记录;断言组从 3 组 55 项增至 9 组 186 项 覆盖配置面全部组合
可维护 新增 tools/bench-scan.ps1 性能基准 性能结论可复现
兼容 edition 2021 → 2024;rust-version = "1.85" —

兼容性:插件 id、descriptor、钩子点、失败策略、目录格式、配置项与环境变量全部不变, 直接替换 dll 即可,无需改任何设置。已在同一套 174 项冒烟断言下与 v0.1.2 逐项对齐。

v0.1.2 —— 修复第二轮审计确认的缺陷

每项都先做了 5 轮重复测试确认稳定复现(非偶发),修复后再做四层回归验证。

缺陷 实际后果
补零兜底被静默吞掉 1 第100话 x 里的前导 1 不补零——第100话 已够长无需改动,却因此让整个文件名的兜底补零被跳过
配置读取 panic 后永久失效 stderr 不可写(如输出端管道已关闭)时,插件在该进程内彻底失效,后续所有任务都不再补零且无法恢复
集数探测不容忍冒号后空格 {"episode_order": 42} 这种合法 JSON 解析失败 → 该任务集数漏统计 → 推断出的位数可能偏小
空目录与「无数据」指纹撞值 「读失败后自动重扫」的自愈逻辑在空目录场景下失效,过期映射可能被一直沿用
两处内部状态误报「已处理」 与第一条同源,同样会让兜底补零被跳过

兼容性:用 336 条真实 episode_title × 4 种宽度共 1344 组数据逐字节对照, 输出与 v0.1.1 完全一致;上述变化只发生在旧版本本来就会处理错的输入上。 安装方式不变,直接替换 dll 即可。

v0.1.1

  • 扩展补零格式:EP1 / Ep 1 / ep.1、行首 [1] / 【2】、分P -P2、日文 第1話 / 第1巻
  • 重写集数探测:一次性建立「合集 → 最大集数」全局映射 + 目录指纹失效, 读取量从 合集数 × 文件数 降到至多 文件数
  • 新增 tools/smoke-test.ps1 端到端冒烟测试(55 项断言)
  • 修复并行审计发现的 6 处隐患:宽度硬顶、descriptor() 纯函数化(去掉其中的磁盘 IO)、 读失败不再盖有效指纹、AUTOPAD_ENABLED=off 语义修正等

v0.1.0

首个版本:第N话 补零 + 从 .下载任务/*.json 自动推断位数。


许可证

MIT,与原项目相同。

vendor/ 目录下的 plugin-api / plugin-sdk 取自 lanyeeee/bilibili-video-downloader(MIT), 原始版权声明见 THIRD-PARTY-NOTICES.md。

该插件系统是实验性 v1:插件以进程内动态库形式运行,与宿主同权限、无沙箱。 装第三方插件时请自行评估代码。

About

lanyeeee/bilibili-video-downloader 插件:下载时自动把集数补零 第1话→第01话,位数按合集集数自动推导 | Bilibili video downloader plugin: auto pad episode numbers

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages