Files
kairee d971e718e9 feat(p3): add discovery, telegram notifications, and one-click favorites
Implement WBI video search with legacy fallback, classification scoring, candidate pool, permanent seen-aid dedupe, blacklist filtering, and batched Telegram notifications. Add write client for fav deal/sort, target-folder selection, insertion position calculation, read-back verification, fav_action locking, retry on failure, ignore and blacklist actions. Wire discovery, candidate cleanup, Telegram long polling, and new discover/candidates CLI commands into the scheduler. Add migration v3 for candidate duration and index stats.
2026-10-31 21:23:12 +08:00

140 lines
4.4 KiB
Python

# -*- coding: utf-8 -*-
"""WBI 签名与搜索解析测试(不联网): uv run python tests/test_search.py"""
import os
import sys
import urllib.parse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdout.reconfigure(encoding="utf-8")
from reaction_board.bilibili.client import BilibiliError, BilibiliProtocolError # noqa: E402
from reaction_board.bilibili.search import ( # noqa: E402
SearchClient,
key_from_url,
mixin_key,
parse_search_payload,
sign_params,
)
IMG_URL = "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png"
SUB_URL = "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png"
NAV_PAYLOAD = {
"code": 0, "message": "0", "ttl": 1,
"data": {
"isLogin": True,
"wbi_img": {"img_url": IMG_URL, "sub_url": SUB_URL},
},
}
SEARCH_PAYLOAD = {
"code": 0, "message": "0", "ttl": 1,
"data": {
"result": [
{
"type": "video",
"id": 117530286889638,
"bvid": "BV1xvxi6LEZD",
"title": '<em class="keyword">咻咻满</em> reaction 初听',
"author": "测试UP",
"mid": 434442072,
"pubdate": 1793370433,
"duration": "105:5",
"pic": "//i0.hdslb.com/bfs/archive/example.jpg",
"play": 12345,
"like": 678,
"favorites": 90,
"danmaku": 12,
"review": 3,
},
{"type": "article", "id": 999, "title": "非视频"},
],
"numPages": 1,
"numResults": 1,
},
}
class FakeClient:
def __init__(self, responses):
self.responses = list(responses)
self.urls = []
def get_json(self, url, *, auth=False):
self.urls.append((url, auth))
item = self.responses.pop(0)
if isinstance(item, BaseException):
raise item
return item
def test_wbi_helpers():
assert key_from_url(IMG_URL) == "7cd084941338484aae1ad9425b84077c"
assert key_from_url(None) == ""
key = mixin_key(
"7cd084941338484aae1ad9425b84077c",
"4932caff0ff746eab6f01bf08b70ac45",
)
assert key == "ea1db124af3c7062474693fa704f4ff8"
signed = sign_params({"keyword": "咻咻满", "page": 1}, key, now=1_700_000_000)
assert signed["wts"] == 1_700_000_000
assert len(signed["w_rid"]) == 32
changed = sign_params({"keyword": "咻咻满", "page": 2}, key, now=1_700_000_000)
assert changed["w_rid"] != signed["w_rid"]
def test_parse_search_payload():
items = parse_search_payload(SEARCH_PAYLOAD)
assert len(items) == 1
item = items[0]
assert item.aid == 117530286889638
assert item.title == "咻咻满 reaction 初听"
assert item.duration_seconds == 105 * 60 + 5
assert item.cover.startswith("https://")
assert item.play == 12345 and item.like == 678
assert item.favorite == 90 and item.danmaku == 12 and item.review == 3
def test_wbi_search_and_fallback():
fake = FakeClient([NAV_PAYLOAD, SEARCH_PAYLOAD])
client = SearchClient(lambda: fake, clock=lambda: 1_700_000_000)
items = client.search("咻咻满 reaction", 1)
assert len(items) == 1
assert "/x/web-interface/nav" in fake.urls[0][0]
assert "/wbi/search/type" in fake.urls[1][0]
query = urllib.parse.parse_qs(urllib.parse.urlparse(fake.urls[1][0]).query)
assert query["w_rid"][0] and query["wts"][0] == "1700000000"
# nav 失败时降级旧接口
fake = FakeClient([BilibiliError("wbi unavailable"), SEARCH_PAYLOAD])
client = SearchClient(lambda: fake, clock=lambda: 1_700_000_000)
items = client.search("咻咻满 reaction", 2)
assert len(items) == 1
assert "/x/web-interface/search/type" in fake.urls[1][0]
def test_missing_result_is_protocol_error():
bad = {"code": 0, "message": "0", "data": {}}
fake = FakeClient([NAV_PAYLOAD, bad, bad])
client = SearchClient(lambda: fake, clock=lambda: 1_700_000_000)
try:
client.search("x", 1)
except BilibiliProtocolError as exc:
assert "result" in str(exc)
else:
raise AssertionError("缺少 result 应报协议错误")
def main():
test_wbi_helpers()
test_parse_search_payload()
test_wbi_search_and_fallback()
test_missing_result_is_protocol_error()
print("OK: WBI 签名、搜索解析与降级测试全部通过")
if __name__ == "__main__":
main()