Files
BiliNote/backend
pumpkinperson996 58ec74f2a2 fix(downloader): yt-dlp 未设 retries 时零重试,网络抖动一次任务即失败
## 问题

任意一次瞬时网络故障都会让整个笔记任务失败,即便立刻重试同一个链接就能成功。
实际遇到的报错:

    ERROR: [download] Got error: HTTPSConnectionPool(
      host='upos-sz-mirrorcosov.bilivideo.com', port=443): Read timed out.

同一个视频一分钟后重新提交,10.18 MiB 四秒下完。

## 原因

yt-dlp 文档里 `retries` 默认 10,但那个默认值是**命令行参数解析器**给的,
Python API 不套用。项目所有 `ydl_opts` 都没设 `retries`,于是:

    # yt_dlp/downloader/http.py
    for retry in RetryManager(self.params.get('retries'), ...)   # -> None
    # yt_dlp/utils/_utils.py
    self.retries = _retries or 0                                 # -> 0

也就是**每次下载只尝试一次,零重试**。这不是 B 站独有的问题,
YouTube 以及所有走 yt-dlp 的路径都一样。

## 改动

- `base.py`:新增 `YDL_RETRY_OPTS`(`retries` / `fragment_retries` /
  `socket_timeout`),两个 downloader 本来就从 base 导入,不额外引入模块。
- `youtube_downloader.py`(2 处)、`bilibili_downloader.py`(3 处):
  所有 `ydl_opts` 都展开该常量。只修报错的那一处会把其余四处继续留在零重试。
- 用的是 yt-dlp 自带的重试机制,没有自写重试循环。

取值偏保守(3 次而非 CLI 的 10):笔记任务是用户在前台等的,
重试太久不如早点失败让用户重来。

## 测试

新增 `tests/test_ydl_retry_opts.py`:

- 行为:常量给出的 RetryManager 预算 > 0;并显式钉住
  `RetryManager(None).retries == 0` 这个被规避的坑。
- 结构:用 AST 断言两个 downloader 里**每一个** `ydl_opts` 字面量都展开了
  `YDL_RETRY_OPTS`,防止以后新增下载路径时又悄悄回到零重试。

结构用例确认过 red-green:去掉任一处展开即失败,并指出具体文件行号。
无 yt-dlp 的环境下两个行为用例自动 skip,与仓库既有测试风格一致。
2026-07-25 12:00:06 -05:00
..