7 Commits
Author SHA1 Message Date
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
pumpkinperson996 3e579b1434 fix(youtube): 修复 YouTube 笔记生成失败 "Requested format is not available"
## 问题

输入 YouTube 链接生成笔记时任务必定失败:

    ERROR: [youtube] <id>: Requested format is not available

即使字幕已经抓取成功(日志里能看到"成功获取 YouTube 字幕,共 N 段"),
任务仍然在下载阶段崩掉。

## 原因

两个独立的问题叠加:

1. `requirements.txt` 把 yt-dlp 钉在 `2025.3.31`。YouTube 之后轮换过
   player,旧版 yt-dlp 解不出 nsig 签名,所有音视频格式都被丢弃,只剩下
   storyboard 图片(日志:`Only images are available for download`),
   格式选择随即抛错。

2. 有字幕时 `NoteGenerator` 走的是"只取元信息"的路径
   (`download(skip_download=True)`,只要 title/duration/cover),但
   `YoutubeDownloader.download` 无条件设置了
   `format='bestaudio[ext=m4a]/bestaudio/best'`。于是一个根本不需要媒体流的
   调用,也会因为选不出格式而失败——把一个字幕已经到手的任务整个带崩。

## 改动

- `youtube_downloader.py`:`skip_download` 时设置
  `ignore_no_formats_error=True`。yt-dlp 落后于 YouTube 时,只取元信息的路径
  降级为"没有音频",而不是让整个笔记失败。
- `youtube_downloader.py`:`ext = info.get("ext") or "m4a"`。跳过下载时
  yt-dlp 返回 `ext=None`,`dict.get` 的默认值对显式 None 不生效,
  会拼出 `xxx.None` 这样的路径。
- `requirements.txt`:`yt-dlp==2025.3.31` → `>=2026.7.4`。yt-dlp 是对抗
  YouTube 变化的滚动依赖,精确钉版本本身就是这个 bug 的成因;用 `>=` 与同文件
  的 `youtube-transcript-api>=1.0.0` 保持一致。
- `bilibili_dm_patch.py`:wrapper 改为透传 `**kwargs`。升级 yt-dlp 后
  `_real_extract` 会以 `fatal=False` 调用 `_download_playinfo`,而 wrapper
  钉死了签名,导致 **所有 B 站下载** 抛
  `TypeError: ... got an unexpected keyword argument 'fatal'`。
- 测试:新增 `test_youtube_metadata_only.py` 覆盖上面两条 YouTube 保证;
  `test_bilibili_dm_patch.py` 新增未知 kwargs 透传用例,并让 fake 响应带上
  `code` 字段(yt-dlp 2026.x 会先校验信封再返回 data)。

## 验证

- 真实跑通:YouTube(有字幕,走元信息路径)与 B 站(无字幕,走完整下载 +
  转写)均能生成笔记。
- `pytest tests/` → 46 passed。唯一失败的
  `test_task_serial_executor` 在升级前后表现一致,与本次改动无关,未作改动。
2026-07-25 07:37:10 -05:00
pumpkinperson996 e5849ca1af Honor explicit Whisper model selection 2026-07-20 20:19:44 -05:00
pumpkinperson996 456ee6037a Fix Whisper model selection caching 2026-07-20 18:46:45 -05:00
pumpkinperson996andClaude Fable 5 75911667a6 fix(frontend): 视频链接缺协议头时自动补 https:// 而非直接拒绝
用户从各处摘抄的链接常常没有 https:// 前缀(如 bilibili.com/...),
表单校验用 new URL() 解析会直接失败,提示"请输入正确的视频链接"。

校验时对无 scheme 的输入先补 https:// 再解析;提交时同样补全后再发
给后端(本地视频路径除外)。已有明确 scheme 的输入不受影响,ftp://
等非 http(s) 协议仍被拒绝。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:00:13 -05:00
pumpkinperson996andClaude Fable 5 944985fc94 fix: 必剪转写实例复用导致的上传状态残留与并发竞争
BcutTranscriber 被 transcriber_provider 缓存复用,但 __etags 等上传
会话状态只在 __init__ 清空、每次上传只追加,导致容器启动后第二次
及以后的转写提交的 etag 数与分片数不符,B 站返回"第三方服务异常"。
并发提交多个视频时,多个后台任务还会在同一实例上交错上传,etag
互相混入。

- _upload() 开头重置全部上传会话状态,修串行残留
- transcript() 加实例锁,整个转写会话串行执行,修并发交错

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:44:47 -05:00
pumpkinperson996andClaude Fable 5 b85d7bc1ff fix: 规范化含 BV 号的任意 B 站链接(稍后再看/收藏夹/追踪参数)
从"稍后再看"(/list/watchlater/?bvid=BV...)、收藏夹播放页等场景
复制的链接会被判为无效链接,尽管其中包含完整 BV 号;校验通过的
链接也会带着追踪参数原样传给 yt-dlp。

在请求入口新增 normalize_video_url():提取 BV 号重建标准
/video/BVxxx 链接,保留分 P 参数(?p=N),丢弃其余查询参数。
b23.tv 短链与其他平台行为不变,无 BV 号链接仍按原逻辑拒绝。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:03:07 -05:00