Skip to content

fix(lms): 流式下载不再被登录页判定整个读进内存 - #80

Draft
fyc09 wants to merge 1 commit into
yan-xiaoo:mainfrom
fyc09:fix/streamed-response-auth-check
Draft

fyc09 wants to merge 1 commit into
yan-xiaoo:mainfrom
fyc09:fix/streamed-response-auth-check

Conversation

@fyc09

@fyc09 fyc09 commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

变更说明

修复 #75 的下载部分(播放那半未修,见末尾)。

思源学堂回放视频约 1 GB,点下载后界面恒停在 0%、磁盘一个字节不写,内存却以下载速率持续上涨。

根因:CommonLoginSession.request 对每个响应都做登录页判定,而 is_auth_failure_response 在 Content-Type 不是 html/text 时会读 response.text。S3 的下载响应恰好没有 Content-Type,于是 stream=True 失效 —— 整个响应先被读进内存,线程才开始写盘。改为对流式响应跳过该判定:stream=True 表示 body 由调用方逐块消费。

验证

  • 已新增回归测试,并确认它在未修复的代码上失败
  • 已运行相关本地分片
  • 合并前 PR Tests / CI Gate 已通过

测试证据

真实 954 MB 回放,用应用自己的 LMSFileDownloadThread:

修复前 修复后
40s 落盘 0 字节 243 MB
RSS 98 → 546 MB 99 → 109 MB
进度 恒 0% 29%
  • 新增 test/sessions/test_streamed_response.py(含“非流式响应仍会判定”的对照)
  • 本地分片:ai 71 / qt-ui 121 / notification 28 / auth-session 69 / schedule 53

未修复:播放

download_url 裸请求返回 200,而 play_url 无 Referer 就 403、带 Referer 后 302 到同一个 S3 链接 —— 换 URL 只会更差。预签名 URL 对 HEAD 一律 403,可能是播放器的障碍。

思源学堂课程回放的视频约 1 GB。点下载后界面一直停在 0%、磁盘一个字节不写,
而内存以下载速率持续上涨(实测 954 MB 的回放:60s 时落盘 0 字节、RSS 从
98 MB 涨到 546 MB,只有 2 条进度信号且恒为 0%)。

根因在 CommonLoginSession.request:它对每个响应都调用 is_auth_failure_response
做登录页判定,而该方法在 Content-Type 不是 html/text 时会读 response.text ——
S3 的下载响应恰好没有 Content-Type,于是 stream=True 完全失效:整个响应先在
内存里被完整读出来,线程才开始写盘。非流式响应在判定之前已被 requests 完整读出,
因此只有流式响应会踩到这一点(全仓库 stream=True 只有 4 处,其中两处走站点会话,
正是 LMS 的单文件下载与批量下载)。

- request() 对流式响应跳过登录页判定:stream=True 表示 body 由调用方逐块消费,
  判定本身没有意义,还会破坏流式语义。
- 新增 test/sessions/test_streamed_response.py(TEST_DOMAIN=auth-session、
  TEST_REGRESSION=True),锁定“流式响应不做判定”并保留“非流式响应仍会判定”的对照;
  已确认该用例在未修复的代码上失败。

实测(真实 954 MB 回放,用应用自己的 LMSFileDownloadThread):
  修复前:40s 落盘 0 字节,RSS 98→546 MB,进度信号 2 条(恒 0%)
  修复后:40s 落盘 243 MB,RSS 99→109 MB,进度推进到 29%

未改动:播放那半(见 PR 说明)。
def _session_with(self, response) -> LMSSession:
session = LMSSession()
session.backend.session.request = Mock(return_value=response)
self.addCleanup(lambda: setattr(session.backend.session, "request", Mock()))

@TiaoFeng TiaoFeng Sep 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-op

没有还原任何东西

  • _session_with 是给 session.backend.session 这个实例挂了一个实例属性 request(遮蔽了 requests.Session.request 类方法);
  • cleanup 只是换了一个新的 Mock,而没有删除该实例属性或恢复原方法。

解除遮蔽、恢复原状

del session.backend.session.request     # 删掉实例属性,.request 重新解析为类方法

作用对象无需清理

  • LMSSession() 是测试方法内新建的局部对象,session 只被当前测试引用。测试结束后它连同被改的 request 属性一起被 GC

建议

Suggested change
self.addCleanup(lambda: setattr(session.backend.session, "request", Mock()))

@TiaoFeng TiaoFeng left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

手动验证

  • 下载回放视频,进度连续增长;
  • 内存平稳,未出现明显占用增加;
  • 在“准备下载”阶段取消能立刻停止。

结论

核心修复正确,可以合并PR。以下 UI 问题均为存量问题、不由本 PR 引入,建议下一步修复

1:resize 会把进度文案重置回“准备下载”

  • 原因:_adjustText()(app/components/ProgressInfoBar.py:189-199)用的是构造时保存的 self.title / self.content;而 onTitleChange / onMessageChange(:319-325)只更新 label,从不同步这两个字段。窗口 resize 触发 _adjustText() 时就会用旧文案覆盖。
  • 修复:两个 slot 内同步字段:
    self.title = text / self.content = text。

2a:进度文案被裁

  • 现象:文案变成 “1.2 MB / 954 MB” 后只显示 “1.2 MB”,尾部被裁;窗口 resize 后才完整。
  • 原因:InfoBar 尺寸只在 show 时按初始短文案(“附件下载/准备下载”)经 _adjustText() → adjustSize() 计算一次;之后文案变长不会重新 fit / 重新贴边,frame 宽度与实际文案不匹配。
  • 修复:在 __initWidget 给 contentLabel 预留最小宽度,让 show 时 bar 就是够宽的,运行期无需变宽:
    metrics = self.contentLabel.fontMetrics()
    self.contentLabel.setMinimumWidth(max(
        metrics.horizontalAdvance("999.99 MB / 999.99 MB"),
        metrics.horizontalAdvance("已下载 999.99 MB"),
    ))

2b:进度条宽度与 InfoBar 不同步

  • 原因:
    • addWidget()(:201-203)强制 Qt.AlignHCenter,进度条不会横向撑满,只按 sizeHint 居中;
    • 构造时用 setFixedWidth(self.width())(:79)硬拉。但同步时机只有
      构造和父窗口 resize(:217-223)两处,文案/frame 宽度变化时不同步;
    • 且 setFixedWidth锁死 min/max,布局无法再控制它。

leo-grayrat added a commit to leo-grayrat/XJTUToolBox that referenced this pull request Sep 25, 2026
- common_session.py: 对 stream=True 的响应跳过登录页判定,修复大文件下载
  时 is_auth_failure_response 将整个响应读入内存导致卡0%的问题(PR yan-xiaoo#80)
- video_page.py: 去掉 S3 预签名 URL 中的 response-content-disposition=attachment
  参数,该参数强制服务器以附件模式返回,破坏 QMediaPlayer 流式播放
- video_page.py: 添加 QMediaPlayer 错误和状态诊断日志

This branch has not been deployed

No deployments
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.

2 participants