Skip to content

fix(downloader): yt-dlp 未设 retries 时零重试,一次网络抖动就让任务失败#428

Open
pumpkinperson996 wants to merge 1 commit into
JefferyHcool:masterfrom
pumpkinperson996:fix/ytdlp-zero-retries
Open

fix(downloader): yt-dlp 未设 retries 时零重试,一次网络抖动就让任务失败#428
pumpkinperson996 wants to merge 1 commit into
JefferyHcool:masterfrom
pumpkinperson996:fix/ytdlp-zero-retries

Conversation

@pumpkinperson996

Copy link
Copy Markdown

#427 相互独立,基于 master,可单独 review / 合并。

问题

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

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

同一个视频(BV12PD5BCEW2)一分钟后重新提交,10.18 MiB 四秒下完,2.29 MiB/s。CDN 抖了一下,任务就没了。

原因

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

# yt_dlp/downloader/http.py
for retry in RetryManager(self.params.get('retries'), self.report_retry):   # -> None

# yt_dlp/utils/_utils.py
class RetryManager:
    def __init__(self, _retries, _error_callback, **kwargs):
        self.retries = _retries or 0                                        # -> 0

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

可以直接验证:

>>> from yt_dlp.utils import RetryManager
>>> RetryManager(None, lambda *a, **k: None).retries
0

改动

文件 改动
base.py 新增 YDL_RETRY_OPTSretries / fragment_retries / socket_timeout
youtube_downloader.py 2 处 ydl_opts 展开该常量
bilibili_downloader.py 3 处 ydl_opts 展开该常量
tests/test_ydl_retry_opts.py 新增

用的是 yt-dlp 自带的重试机制,没有自写重试循环。常量放在 base.py 是因为两个 downloader 本来就从它导入,不额外引入模块。

只改报错的那一处(B 站音频下载)会把其余四处继续留在零重试,所以 5 处全部覆盖。

取值说明:3 次而不是 CLI 的 10。笔记任务是用户在前台等着的,重试太久不如早点失败让用户重来。维护者觉得该更耐受的话,改常量一行即可。

测试

  • 行为:断言常量给出的 RetryManager 预算 > 0;并显式钉住 RetryManager(None).retries == 0,把被规避的那个坑写进测试当文档。
  • 结构:用 AST 断言两个 downloader 里每一个 ydl_opts 字面量都展开了 YDL_RETRY_OPTS。这条是防止以后新增下载路径时又悄悄回到零重试——失败信息会直接指出文件和行号:
AssertionError: 'YDL_RETRY_OPTS' not found in set() :
bilibili_downloader.py:65 builds yt-dlp options without YDL_RETRY_OPTS, so it gets zero retries

结构用例确认过 red-green(去掉任一处展开即失败)。无 yt-dlp 的环境下两个行为用例自动 skip,与仓库既有测试的写法一致。

验证

改动在 Docker 环境里跑通过:retries 确实到达 RetryManager(预算 0 → 3),YouTube 与 B 站均能正常出笔记,其中 B 站用的正是上面那个曾经超时失败的视频。

需要说明的是,重试本身只在真实瞬时故障发生时才会触发,我没有人为构造网络故障去观测重试计数——测试钉的是"预算不为 0"这个前置条件。

## 问题

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

    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,与仓库既有测试风格一致。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant