diff --git a/media_platform/douyin/client.py b/media_platform/douyin/client.py index 759920e..cfa095e 100644 --- a/media_platform/douyin/client.py +++ b/media_platform/douyin/client.py @@ -214,7 +214,19 @@ class DouYinClient(AbstractApiClient, ProxyRefreshMixin): :param aweme_id: :return: """ - params = {"aweme_id": aweme_id} + # 抖音 detail 接口的 Argus 风控要求这两个参数成套出现,缺一则直接 403 + # (响应体为 "Blocked by ArgusSecurityPlugin Uifid Not Found"): + # uifid = UIFID cookie,没有时退到 UIFID_TEMP + # verifyFp / fp = s_v_web_id cookie + # 必须用 cookie 里的 s_v_web_id:实测 uifid 搭配自生成的 verifyFp 会被判成 + # "Signature Not Found",两者同源才能通过。 + s_v_web_id = self.cookie_dict.get("s_v_web_id", "") + params = { + "aweme_id": aweme_id, + "uifid": self.cookie_dict.get("UIFID") or self.cookie_dict.get("UIFID_TEMP", ""), + "verifyFp": s_v_web_id, + "fp": s_v_web_id, + } headers = copy.copy(self.headers) del headers["Origin"] res = await self.get("/aweme/v1/web/aweme/detail/", params, headers) diff --git a/tests/test_douyin_aweme_detail_params.py b/tests/test_douyin_aweme_detail_params.py new file mode 100644 index 0000000..dcac1ec --- /dev/null +++ b/tests/test_douyin_aweme_detail_params.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2025 relakkes@gmail.com +# +# This file is part of MediaCrawler project. +# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/tests/test_douyin_aweme_detail_params.py +# GitHub: https://github.com/NanmiCoder +# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1 +# + +"""抖音 aweme detail 接口的风控参数回归测试。 + +回归背景:上游 detail 接口的 Argus 风控要求 ``uifid`` / ``verifyFp`` / ``fp`` +三个参数,缺一时直接 403,响应体为 +``Blocked by ArgusSecurityPlugin Uifid Not Found``(补上 uifid 但 verifyFp 不对时 +换成 ``... Signature Not Found``)。原先只传 aweme_id,于是详情全线失败, +连带媒体也拿不到。 + +这里不发起任何网络请求,只锁定这三个参数确实被带上、且取值来自浏览器 cookie。 +""" + +from __future__ import annotations + +import pytest + +from media_platform.douyin.client import DouYinClient + +AWEME_ID = "7525538910311632128" + + +class _StubPage: + """占位 page,仅用于构造 client;本测试不会走到 playwright 调用""" + + async def evaluate(self, expression): # noqa: ANN001 + return {} + + +def _make_client(cookie_dict: dict) -> DouYinClient: + return DouYinClient( + headers={ + "User-Agent": "test-user-agent", + "Cookie": "a=1", + "Origin": "https://www.douyin.com/", + }, + playwright_page=_StubPage(), + cookie_dict=cookie_dict, + ) + + +@pytest.mark.asyncio +async def test_aweme_detail_sends_uifid_and_verify_fp(monkeypatch): + """uifid 与 verifyFp/fp 必须成套出现,且都取自 cookie""" + captured: dict = {} + + async def fake_get(uri, params=None, headers=None): # noqa: ANN001 + captured["uri"] = uri + captured["params"] = dict(params or {}) + return {"aweme_detail": {"aweme_id": AWEME_ID}} + + client = _make_client( + {"s_v_web_id": "verify_test_fp", "UIFID": "uifid-from-cookie"} + ) + monkeypatch.setattr(client, "get", fake_get) + + await client.get_video_by_id(AWEME_ID) + + assert captured["uri"] == "/aweme/v1/web/aweme/detail/" + assert captured["params"]["aweme_id"] == AWEME_ID + assert captured["params"]["uifid"] == "uifid-from-cookie" + # verifyFp 与 fp 必须同源,且用 cookie 里的 s_v_web_id(自生成的会被判 Signature Not Found) + assert captured["params"]["verifyFp"] == "verify_test_fp" + assert captured["params"]["fp"] == "verify_test_fp" + + +@pytest.mark.asyncio +async def test_aweme_detail_falls_back_to_uifid_temp(monkeypatch): + """没有 UIFID 时退到 UIFID_TEMP""" + captured: dict = {} + + async def fake_get(uri, params=None, headers=None): # noqa: ANN001 + captured["params"] = dict(params or {}) + return {"aweme_detail": {}} + + client = _make_client({"s_v_web_id": "fp", "UIFID_TEMP": "temp-only"}) + monkeypatch.setattr(client, "get", fake_get) + + await client.get_video_by_id(AWEME_ID) + + assert captured["params"]["uifid"] == "temp-only" + + +@pytest.mark.asyncio +async def test_aweme_detail_without_cookies_still_requests(monkeypatch): + """cookie 缺失时不能抛异常,参数退化为空串(由服务端决定是否放行)""" + captured: dict = {} + + async def fake_get(uri, params=None, headers=None): # noqa: ANN001 + captured["params"] = dict(params or {}) + return {"aweme_detail": {}} + + client = _make_client({}) + monkeypatch.setattr(client, "get", fake_get) + + await client.get_video_by_id(AWEME_ID) + + assert captured["params"]["uifid"] == "" + assert captured["params"]["verifyFp"] == "" + assert captured["params"]["fp"] == ""