refactor(favsort): send bilibili requests through httpx

Reuse a pooled httpx client for favsort GET and POST requests, share the raw-request-derived browser headers, and add folder-specific referers for ids, infos and sort. Add transport tests for header parity and anonymous-to-authenticated fallback.
This commit is contained in:
kairee committed 2026-10-31 22:36:48 +08:00
1 parent 5b22ed1c5d
commit 8a87ebfb7c
5 files changed
+160 -22

No files matched your search

+64 -20
View File
@@ -28,7 +28,8 @@ import random
import sys
import time
import urllib.parse
import urllib.request
import httpx
sys.stdout.reconfigure(encoding="utf-8")
@@ -95,41 +96,67 @@ def load_config(require_auth=True):
# ---------------------------------------------------------------- http
def _get_json(cfg, url, auth):
req = urllib.request.Request(url, headers=make_headers(cfg, auth=auth))
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read().decode("utf-8", "replace"))
_HTTP_CLIENT: httpx.Client | None = None
def get_json(cfg, url, auth=False):
def _http_client() -> httpx.Client:
global _HTTP_CLIENT
if _HTTP_CLIENT is None:
_HTTP_CLIENT = httpx.Client(
timeout=httpx.Timeout(30, connect=10),
limits=httpx.Limits(
max_keepalive_connections=5,
keepalive_expiry=30,
),
follow_redirects=False,
trust_env=True,
)
return _HTTP_CLIENT
def _get_json(cfg, url, auth, referer=None):
response = _http_client().get(
url,
headers=make_headers(cfg, auth=auth, referer=referer),
timeout=30,
)
response.raise_for_status()
return response.json()
def get_json(cfg, url, auth=False, referer=None):
"""只读请求:默认用去掉账号身份的 cookie。
万一匿名请求被拒(HTTP 错误,或 code != 0),自动带完整 cookie 重试一次,
保证功能不会因为剥离身份而回退。
"""
if auth:
return _get_json(cfg, url, auth=True)
return _get_json(cfg, url, auth=True, referer=referer)
try:
r = _get_json(cfg, url, auth=False)
r = _get_json(cfg, url, auth=False, referer=referer)
if r.get("code") == 0:
return r
except Exception: # noqa: BLE001 网络错误 / 412 风控都走回退
pass
return _get_json(cfg, url, auth=True)
return _get_json(cfg, url, auth=True, referer=referer)
def post_form(cfg, url, data):
headers = make_headers(cfg, auth=True)
def post_form(cfg, url, data, referer=None):
headers = make_headers(cfg, auth=True, referer=referer)
headers["content-type"] = "application/x-www-form-urlencoded"
req = urllib.request.Request(
url, data=urllib.parse.urlencode(data).encode(), headers=headers, method="POST"
response = _http_client().post(
url, headers=headers, data=data, timeout=30
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read().decode("utf-8", "replace"))
response.raise_for_status()
return response.json()
def fetch_ids(cfg, media_id):
r = get_json(cfg, API["ids"].format(base=BASE, mid=media_id))
referer = (
f"https://space.bilibili.com/{cfg['mid']}/favlist"
f"?fid={media_id}&ftype=create"
)
r = get_json(cfg, API["ids"].format(base=BASE, mid=media_id), referer=referer)
if r.get("code") != 0:
raise SystemExit(f"ids 失败: code={r.get('code')} msg={r.get('message')}")
return r.get("data") or []
@@ -137,13 +164,17 @@ def fetch_ids(cfg, media_id):
def fetch_infos(cfg, media_id, folder_mid, id_list):
items, calls = [], 0
referer = (
f"https://space.bilibili.com/{folder_mid}/favlist"
f"?fid={media_id}&ftype=create"
)
for i in range(0, len(id_list), CHUNK):
chunk = id_list[i:i + CHUNK]
res = ",".join(f"{x['id']}:{x['type']}" for x in chunk)
url = API["infos"].format(
base=BASE, res=urllib.parse.quote(res), fmid=folder_mid, mid=media_id
)
r = get_json(cfg, url)
r = get_json(cfg, url, referer=referer)
calls += 1
if r.get("code") != 0:
raise SystemExit(f"infos 失败 @{i}: code={r.get('code')} msg={r.get('message')}")
@@ -310,7 +341,12 @@ def load_plan(media_id):
def cmd_folders(cfg, args):
# 注意:list-all 是"列出我自己的收藏夹",匿名调用只会返回公开收藏夹(实测 11 -> 3),
# 因此这里必须带完整 cookie(仍然只是 GET,不会改动任何数据)。
r = get_json(cfg, API["folders"].format(base=BASE, mid=cfg["mid"]), auth=True)
r = get_json(
cfg,
API["folders"].format(base=BASE, mid=cfg["mid"]),
auth=True,
referer=f"https://space.bilibili.com/{cfg['mid']}/favlist",
)
if r.get("code") != 0:
raise SystemExit(f"获取收藏夹列表失败: {r.get('code')} {r.get('message')}")
lst = (r.get("data") or {}).get("list") or []
@@ -431,6 +467,10 @@ def cmd_run(cfg, args):
work = list(actual)
total = len(moves)
long_after = random.randint(sc["long_every_min"], sc["long_every_max"])
sort_referer = (
f"https://space.bilibili.com/{cfg['mid']}/favlist"
f"?fid={media_id}&ftype=create"
)
for i, m in enumerate(moves, 1):
pred_type = types.get(m["pred"], 2)
@@ -442,14 +482,18 @@ def cmd_run(cfg, args):
}
print(f"[run] {i}/{total} {titles[str(m['moved'])]['title'][:30]} "
f"-> after {m['pred']}", end=" ", flush=True)
resp = post_form(cfg, API["sort"].format(base=BASE), body)
resp = post_form(
cfg, API["sort"].format(base=BASE), body, referer=sort_referer
)
code = resp.get("code")
print(f"code={code}")
if code != 0:
wait = sc["retry_wait"]
print(f"[run] 返回非 0,冷却 {wait}s 后重试一次")
time.sleep(wait)
resp = post_form(cfg, API["sort"].format(base=BASE), body)
resp = post_form(
cfg, API["sort"].format(base=BASE), body, referer=sort_referer
)
code = resp.get("code")
print(f"[run] retry code={code}")
if code != 0: