Merge pull request #410 from JefferyHcool/fix/bilibili-412-dm-img-params

fix(bilibili): 注入 dm_img 风控参数修复 wbi/playurl 412
This commit is contained in:
Jianwu Huang
2026-06-17 09:58:37 +08:00
committed by GitHub
3 changed files with 171 additions and 0 deletions

View 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()