mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-19 14:40:12 +08:00
fix(bilibili): 注入 dm_img 风控参数修复 wbi/playurl 412
B 站 wbi/playurl 网关新增 dm_img_list/dm_img_str/dm_cover_img_str/ dm_img_inter + web_location 风控校验,缺失即返回 HTTP 412。对于网页不内嵌 playinfo、必须走 API 的视频(如 BV1X9L16oEgB),yt-dlp(含最新版)尚未适配, 导致下载失败,且刷新 cookie 无效。 通过猴补丁在 BilibiliBaseIE._download_playinfo 的 wbi 签名前注入哑值 dm_img 参数(取值形态对齐 yt-dlp 自身在 arc/search 端点的用法),即可恢复 200。 已验证补丁对固定版 2025.03.31 与最新 2026.06.09 签名一致、向前兼容;新增 4 个单元测试。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
94
backend/tests/test_bilibili_dm_patch.py
Normal file
94
backend/tests/test_bilibili_dm_patch.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
TDD coverage for the Bilibili wbi/playurl dm_img risk-control patch.
|
||||
|
||||
Background: around 2026-06, Bilibili's `x/player/wbi/playurl` gateway began
|
||||
rejecting requests that omit the browser fingerprint params
|
||||
(dm_img_list / dm_img_str / dm_cover_img_str / dm_img_inter + web_location)
|
||||
with HTTP 412. yt-dlp (incl. latest) does not yet send these for playurl, so
|
||||
videos whose web page does not inline playinfo (forcing the API call) fail.
|
||||
|
||||
These tests verify our yt-dlp monkey-patch injects those params *before* wbi
|
||||
signing, and that caller-supplied query params still win.
|
||||
"""
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
MODULE_PATH = ROOT / "app" / "downloaders" / "bilibili_dm_patch.py"
|
||||
spec = importlib.util.spec_from_file_location("bilibili_dm_patch", MODULE_PATH)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError("bilibili_dm_patch module spec not found")
|
||||
bilibili_dm_patch = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(bilibili_dm_patch)
|
||||
|
||||
REQUIRED_KEYS = {
|
||||
"web_location",
|
||||
"dm_img_list",
|
||||
"dm_img_str",
|
||||
"dm_cover_img_str",
|
||||
"dm_img_inter",
|
||||
}
|
||||
|
||||
|
||||
class BuildDmImgParamsTest(unittest.TestCase):
|
||||
def test_contains_all_required_risk_control_keys(self):
|
||||
params = bilibili_dm_patch.build_dm_img_params()
|
||||
self.assertTrue(REQUIRED_KEYS.issubset(params.keys()))
|
||||
|
||||
def test_web_location_is_expected_sentinel(self):
|
||||
self.assertEqual(bilibili_dm_patch.build_dm_img_params()["web_location"], 1550101)
|
||||
|
||||
|
||||
class ApplyPatchTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
try:
|
||||
import yt_dlp.extractor.bilibili # noqa: F401
|
||||
except Exception as exc: # pragma: no cover - env without yt-dlp
|
||||
self.skipTest(f"yt-dlp not importable: {exc}")
|
||||
|
||||
def test_patch_is_idempotent(self):
|
||||
from yt_dlp.extractor.bilibili import BilibiliBaseIE
|
||||
|
||||
self.assertTrue(bilibili_dm_patch.apply_bilibili_dm_img_patch())
|
||||
first = BilibiliBaseIE._download_playinfo
|
||||
self.assertTrue(bilibili_dm_patch.apply_bilibili_dm_img_patch())
|
||||
self.assertIs(BilibiliBaseIE._download_playinfo, first)
|
||||
|
||||
def test_dm_params_reach_wbi_signing_with_caller_query_preserved(self):
|
||||
from yt_dlp import YoutubeDL
|
||||
from yt_dlp.extractor.bilibili import BilibiliBaseIE
|
||||
|
||||
bilibili_dm_patch.apply_bilibili_dm_img_patch()
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_sign_wbi(params, video_id):
|
||||
# Capture the exact params handed to wbi signing (just before the
|
||||
# HTTP request). dm_* must already be present here, pre-signature.
|
||||
captured.update(params)
|
||||
return params
|
||||
|
||||
def fake_download_json(url, video_id, **kwargs):
|
||||
# Avoid any network; the real playurl call would 412 without dm_*.
|
||||
return {"data": {"ok": True}}
|
||||
|
||||
ie = BilibiliBaseIE(YoutubeDL({"quiet": True}))
|
||||
ie._sign_wbi = fake_sign_wbi
|
||||
ie._download_json = fake_download_json
|
||||
|
||||
ie._download_playinfo("BV1X9L16oEgB", 4242, headers={}, query={"qn": 64})
|
||||
|
||||
self.assertTrue(
|
||||
REQUIRED_KEYS.issubset(captured.keys()),
|
||||
f"missing dm_* keys, got: {sorted(captured)}",
|
||||
)
|
||||
self.assertEqual(captured["web_location"], 1550101)
|
||||
# caller-supplied query must survive the merge
|
||||
self.assertEqual(captured["qn"], 64)
|
||||
# the original method still builds its base params
|
||||
self.assertEqual(captured["bvid"], "BV1X9L16oEgB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user