feat(p0): add favorite folder sync and sqlite data foundation
Implement P0 of the reaction dashboard: config loading with hot reload, SQLite migrations with backups, single-writer queue, read-only Bilibili fav client, folder import/diff sync, invalid-resource handling, blacklist seed, scheduler, and JSON redacted logging. Includes offline unit and transport-level contract tests; all existing and new tests pass.
This commit is contained in:
commit
e2cdf997af
39 files changed
+8575
No files matched your search
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""只读接口的身份校验探测:对比 完整 cookie / 无 cookie / 去掉身份字段 三种情况。
|
||||
|
||||
用法: python tools/probe_anon.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from favsort import strip_identity # noqa: E402 与主程序保持同一套剥离规则
|
||||
|
||||
BASE = "https://api.bilibili.com"
|
||||
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36")
|
||||
|
||||
BASE_HEADERS = {
|
||||
"accept": "*/*",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"dnt": "1",
|
||||
"origin": "https://www.bilibili.com",
|
||||
"referer": "https://www.bilibili.com/",
|
||||
"sec-ch-ua": '"Google Chrome";v="155", "Chromium";v="155", "Not(A:Brand";v="24"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-site",
|
||||
"user-agent": UA,
|
||||
}
|
||||
|
||||
|
||||
def get(url, cookie):
|
||||
headers = dict(BASE_HEADERS)
|
||||
if cookie is not None:
|
||||
headers["cookie"] = cookie
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.loads(r.read().decode("utf-8", "replace"))
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"code": "EXC", "message": str(e)[:60]}
|
||||
|
||||
|
||||
def main():
|
||||
cfg = json.load(open(os.path.join(ROOT, "config.json"), encoding="utf-8"))
|
||||
full = cfg["cookie"]
|
||||
anon = strip_identity(full)
|
||||
|
||||
variants = [("完整 cookie", full), ("去掉身份字段", anon), ("完全无 cookie", None)]
|
||||
folders = [(3700023351, "咻咻满 reaction 合集 (658)"),
|
||||
(4451053851, "咻咻满-reaction-2025 (167)"),
|
||||
(85420451, "默认收藏夹 (29)")]
|
||||
|
||||
print("=== ids 接口 ===")
|
||||
for name, ck in variants:
|
||||
for mid, label in folders:
|
||||
url = f"{BASE}/x/v3/fav/resource/ids?media_id={mid}&platform=web"
|
||||
r = get(url, ck)
|
||||
n = len(r.get("data") or []) if isinstance(r.get("data"), list) else "-"
|
||||
print(f" [{name:<14}] media={mid:<11} code={r.get('code')!s:<5} "
|
||||
f"条数={n!s:<5} msg={r.get('message')}")
|
||||
time.sleep(1.2)
|
||||
print()
|
||||
|
||||
print("=== infos 接口(各取前 3 条)===")
|
||||
for mid, label in folders:
|
||||
ids = get(f"{BASE}/x/v3/fav/resource/ids?media_id={mid}&platform=web", full)
|
||||
arr = ids.get("data") or []
|
||||
res = ",".join(f"{x['id']}:{x['type']}" for x in arr[:3])
|
||||
if not res:
|
||||
print(f" media={mid}: 没有可测资源")
|
||||
continue
|
||||
for name, ck in variants:
|
||||
url = (f"{BASE}/x/v3/fav/resource/infos?resources={urllib.parse.quote(res)}"
|
||||
f"&platform=web&folder_mid={cfg['mid']}&folder_id={mid}")
|
||||
r = get(url, ck)
|
||||
d = r.get("data")
|
||||
n = len(d) if isinstance(d, list) else "-"
|
||||
first = (d[0].get("title", "")[:18] if isinstance(d, list) and d else "")
|
||||
print(f" [{name:<14}] media={mid:<11} code={r.get('code')!s:<5} "
|
||||
f"返回={n!s:<3} msg={r.get('message')} {first}")
|
||||
time.sleep(1.2)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user