feat(auth): separate read and write bilibili accounts
This commit is contained in:
1 parent
5c2ab7b187
commit
178ab64b18
17 files changed
+671
-707
No files matched your search
@@ -0,0 +1,249 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""扫码授权流程;仅由 ``python app.py auth`` 调用。
|
||||
|
||||
认证结果只写入指定的 ``bilibili.read`` 或 ``bilibili.write`` 段,两个账号的
|
||||
身份及设备指纹 Cookie 绝不互相覆盖。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
from .auth import (
|
||||
IDENTITY_KEYS,
|
||||
cookie_value,
|
||||
cookies_from_set_cookie,
|
||||
gen_uuid,
|
||||
make_headers,
|
||||
merge_cookie,
|
||||
parse_cookie,
|
||||
sessdata_expiry,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
VENDOR = ROOT / "vendor"
|
||||
if str(VENDOR) not in sys.path:
|
||||
sys.path.insert(0, str(VENDOR))
|
||||
import qrcodegen # noqa: E402
|
||||
|
||||
BASE = "https://api.bilibili.com"
|
||||
PASSPORT = "https://passport.bilibili.com"
|
||||
|
||||
|
||||
def write_qr_png(text: str, path: str | Path, scale: int = 8, border: int = 4) -> Path:
|
||||
"""将登录链接编码为不依赖图像库的 8-bit 灰度 QR PNG。"""
|
||||
qr = qrcodegen.QrCode.encode_text(text, qrcodegen.QrCode.Ecc.MEDIUM)
|
||||
size = qr.get_size()
|
||||
dim = (size + border * 2) * scale
|
||||
rows: list[bytes] = []
|
||||
for y in range(dim):
|
||||
my = y // scale - border
|
||||
row = bytearray([0])
|
||||
for x in range(dim):
|
||||
mx = x // scale - border
|
||||
dark = 0 <= mx < size and 0 <= my < size and qr.get_module(mx, my)
|
||||
row.append(0 if dark else 255)
|
||||
rows.append(bytes(row))
|
||||
|
||||
def chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(
|
||||
">I", zlib.crc32(tag + data) & 0xFFFFFFFF
|
||||
)
|
||||
|
||||
output = Path(path)
|
||||
output.write_bytes(
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", dim, dim, 8, 0, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(b"".join(rows), 9))
|
||||
+ chunk(b"IEND", b"")
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def open_path(path: Path) -> bool:
|
||||
if os.name == "nt":
|
||||
os.startfile(path) # type: ignore[attr-defined]
|
||||
return True
|
||||
return webbrowser.open(path.as_uri())
|
||||
|
||||
|
||||
def ensure_device_cookies(
|
||||
credentials: dict[str, Any], get_json: Callable[[str, bool], Mapping[str, Any]]
|
||||
) -> list[str]:
|
||||
"""仅为当前账号补齐缺失设备字段,已有指纹不改动。"""
|
||||
pairs = parse_cookie(str(credentials.get("cookie") or ""))
|
||||
lower = {key.lower() for key, _ in pairs}
|
||||
added: list[str] = []
|
||||
if "buvid3" not in lower or "buvid4" not in lower:
|
||||
try:
|
||||
data = get_json(f"{BASE}/x/frontend/finger/spi", True).get("data") or {}
|
||||
except Exception: # noqa: BLE001
|
||||
data = {}
|
||||
if data.get("b_3") and "buvid3" not in lower:
|
||||
pairs.append(("buvid3", str(data["b_3"])))
|
||||
lower.add("buvid3")
|
||||
added.append("buvid3")
|
||||
if data.get("b_4") and "buvid4" not in lower:
|
||||
pairs.append(("buvid4", str(data["b_4"])))
|
||||
lower.add("buvid4")
|
||||
added.append("buvid4")
|
||||
if "_uuid" not in lower:
|
||||
pairs.append(("_uuid", gen_uuid()))
|
||||
lower.add("_uuid")
|
||||
added.append("_uuid")
|
||||
if "b_nut" not in lower and "buvid3" in added:
|
||||
pairs.append(("b_nut", str(int(time.time()))))
|
||||
credentials["cookie"] = "; ".join(f"{key}={value}" for key, value in pairs)
|
||||
return added
|
||||
|
||||
|
||||
class AuthService:
|
||||
def __init__(self, config_path: str | Path, *, account: str, logger=None) -> None:
|
||||
if account not in {"read", "write"}:
|
||||
raise ValueError(f"未知账号角色: {account}")
|
||||
self.path = Path(config_path)
|
||||
self.account = account
|
||||
self.logger = logger
|
||||
|
||||
@property
|
||||
def backup_path(self) -> Path:
|
||||
return self.path.with_name(self.path.name + ".bak")
|
||||
|
||||
def _load_raw(self) -> dict[str, Any]:
|
||||
with self.path.open(encoding="utf-8") as fh:
|
||||
raw = json.load(fh)
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("配置文件顶层必须是对象")
|
||||
section = raw.get("bilibili")
|
||||
if not isinstance(section, dict) or not isinstance(section.get(self.account), dict):
|
||||
raise ValueError(f"缺少 bilibili.{self.account} 账号配置")
|
||||
return raw
|
||||
|
||||
@staticmethod
|
||||
def _request(credentials: Mapping[str, Any], url: str, *, auth: bool) -> tuple[dict[str, Any], list[str]]:
|
||||
req = urllib.request.Request(url, headers=make_headers(dict(credentials), auth=auth))
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
return (
|
||||
json.loads(response.read().decode("utf-8", "replace")),
|
||||
response.headers.get_all("Set-Cookie") or [],
|
||||
)
|
||||
|
||||
def _get_json(self, credentials: Mapping[str, Any], url: str, auth: bool) -> Mapping[str, Any]:
|
||||
return self._request(credentials, url, auth=auth)[0]
|
||||
|
||||
def _write_raw(self, raw: Mapping[str, Any]) -> None:
|
||||
tmp = self.path.with_name(self.path.name + ".tmp")
|
||||
with tmp.open("w", encoding="utf-8") as fh:
|
||||
json.dump(raw, fh, ensure_ascii=False, indent=2)
|
||||
fh.write("\n")
|
||||
os.replace(tmp, self.path)
|
||||
|
||||
def finish(self, raw: dict[str, Any], pairs: Mapping[str, str]) -> int:
|
||||
"""写入单个账号、验证 nav;验证失败还原整个原配置文件。"""
|
||||
credentials = raw["bilibili"][self.account]
|
||||
old_cookie = str(credentials.get("cookie") or "")
|
||||
merged = merge_cookie(old_cookie, dict(pairs))
|
||||
if not cookie_value(merged, "SESSDATA") or not cookie_value(merged, "bili_jct"):
|
||||
print("[auth] 合并后缺少 SESSDATA / bili_jct,中止")
|
||||
return 2
|
||||
credentials["cookie"] = merged
|
||||
credentials["csrf"] = cookie_value(merged, "bili_jct")
|
||||
added = ensure_device_cookies(
|
||||
credentials,
|
||||
lambda url, auth: self._get_json(credentials, url, auth),
|
||||
)
|
||||
new_mid = cookie_value(str(credentials["cookie"]), "DedeUserID")
|
||||
if new_mid.isdigit():
|
||||
credentials["mid"] = int(new_mid)
|
||||
|
||||
shutil.copy2(self.path, self.backup_path)
|
||||
self._write_raw(raw)
|
||||
try:
|
||||
nav = self._get_json(credentials, f"{BASE}/x/web-interface/nav", True)
|
||||
data = nav.get("data") or {}
|
||||
ok = nav.get("code") == 0 and bool(data.get("isLogin"))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
data, ok = {"uname": str(exc)}, False
|
||||
if not ok:
|
||||
shutil.copy2(self.backup_path, self.path)
|
||||
print(f"[auth] {self.account} 登录校验失败,已回滚 config.json")
|
||||
return 4
|
||||
|
||||
if isinstance(data.get("mid"), int):
|
||||
credentials["mid"] = data["mid"]
|
||||
self._write_raw(raw)
|
||||
updated = sorted(key for key in pairs if key.lower() in IDENTITY_KEYS)
|
||||
kept = [key for key in ("buvid3", "buvid4", "_uuid", "buvid_fp")
|
||||
if cookie_value(str(credentials["cookie"]), key)]
|
||||
print(f"[auth] {self.account} 已更新身份字段: {', '.join(updated)}")
|
||||
if added:
|
||||
print(f"[auth] {self.account} 补齐设备字段: {', '.join(added)}")
|
||||
print(f"[auth] {self.account} 保留设备指纹: {', '.join(kept) if kept else '(无)'}")
|
||||
expiry = sessdata_expiry(str(credentials["cookie"]))
|
||||
if expiry:
|
||||
print(f"[auth] 新 SESSDATA 到期: {time.strftime('%Y-%m-%d', time.localtime(expiry))}")
|
||||
print(f"[auth] {self.account} 校验通过 ✅ mid={credentials['mid']} uname={data.get('uname')}")
|
||||
return 0
|
||||
|
||||
def run(self, *, timeout: int, no_browser: bool) -> int:
|
||||
raw = self._load_raw()
|
||||
credentials = raw["bilibili"][self.account]
|
||||
print(f"[auth] 为 {self.account} 账号申请扫码登录 ...")
|
||||
generated, _ = self._request(
|
||||
credentials, f"{PASSPORT}/x/passport-login/web/qrcode/generate?source=main-fe-header", auth=False
|
||||
)
|
||||
data = generated.get("data") or {}
|
||||
key, login_url = data.get("qrcode_key"), data.get("url")
|
||||
if generated.get("code") != 0 or not key or not login_url:
|
||||
print(f"[auth] 申请二维码失败: code={generated.get('code')} {generated.get('message')}")
|
||||
return 2
|
||||
qr_path = Path(tempfile.gettempdir()) / f"bili-fav-{self.account}-{key[:8]}.png"
|
||||
write_qr_png(str(login_url), qr_path)
|
||||
print(f"[auth] 二维码已生成: {qr_path}")
|
||||
if not no_browser:
|
||||
try:
|
||||
open_path(qr_path)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"[auth] 打开图片失败: {exc},请手动打开二维码")
|
||||
deadline, last = time.time() + timeout, None
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
payload, set_cookies = self._request(
|
||||
credentials,
|
||||
f"{PASSPORT}/x/passport-login/web/qrcode/poll?qrcode_key={urllib.parse.quote(str(key))}",
|
||||
auth=False,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"[auth] 轮询异常: {exc}")
|
||||
time.sleep(3)
|
||||
continue
|
||||
poll = payload.get("data") or {}
|
||||
state = poll.get("code")
|
||||
if state == 0:
|
||||
pairs = cookies_from_set_cookie(set_cookies)
|
||||
for part in urllib.parse.urlparse(str(poll.get("url") or "")).query.split("&"):
|
||||
key_name, _, value = part.partition("=")
|
||||
if key_name and value:
|
||||
pairs.setdefault(key_name, value)
|
||||
return self.finish(raw, pairs)
|
||||
if state == 86038:
|
||||
print("[auth] 二维码已过期,请重新运行 auth")
|
||||
return 3
|
||||
if state != last:
|
||||
print(f"[auth] {(poll.get('message') or {86101: '未扫码', 86090: '已扫码,等待确认'}.get(state, f'状态 {state}'))} ...")
|
||||
last = state
|
||||
time.sleep(2)
|
||||
print("[auth] 等待超时,二维码已失效,请重新运行 auth")
|
||||
return 3
|
||||
+48
-20
@@ -19,6 +19,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
|
||||
from .bilibili.auth_flow import AuthService
|
||||
from .bilibili.client import BilibiliClient, Pacer
|
||||
from .config import ConfigError, ConfigStore
|
||||
from .database import Database
|
||||
@@ -58,7 +59,15 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
snapshot.add_argument(
|
||||
"--force", action="store_true", help="忽略当日完成标记,重新采集"
|
||||
)
|
||||
sub.add_parser("auth-check", parents=[common], help="检查 Cookie 登录态")
|
||||
auth = sub.add_parser("auth", parents=[common], help="扫码授权并更新指定账号 Cookie")
|
||||
auth.add_argument("--account", choices=("read", "write"), default="read",
|
||||
help="要授权的账号角色(默认 read)")
|
||||
auth.add_argument("--no-browser", action="store_true", help="不自动打开二维码图片")
|
||||
auth.add_argument("--timeout", type=int, default=180, metavar="SEC",
|
||||
help="等待扫码的超时时间(默认 180 秒)")
|
||||
auth_check = sub.add_parser("auth-check", parents=[common], help="检查账号登录态")
|
||||
auth_check.add_argument("--account", choices=("read", "write", "all"), default="read",
|
||||
help="要检查的账号角色(默认 read)")
|
||||
run = sub.add_parser("run", parents=[common], help="按配置周期常驻运行同步与快照")
|
||||
run.add_argument(
|
||||
"--once", action="store_true", help="只执行一次到期任务后退出"
|
||||
@@ -82,11 +91,11 @@ def _print(payload) -> None:
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2), flush=True)
|
||||
|
||||
|
||||
def _client(cfg, logger: JsonLogger, *, pacer=None, retries: int = 3) -> BilibiliClient:
|
||||
def _client(credentials, logger: JsonLogger, *, pacer=None, retries: int = 3) -> BilibiliClient:
|
||||
return BilibiliClient(
|
||||
cookie=cfg.bilibili.cookie,
|
||||
csrf=cfg.bilibili.csrf,
|
||||
mid=cfg.bilibili.mid,
|
||||
cookie=credentials.cookie,
|
||||
csrf=credentials.csrf,
|
||||
mid=credentials.mid,
|
||||
pacer=pacer,
|
||||
retries=retries,
|
||||
logger=logger,
|
||||
@@ -120,7 +129,7 @@ def cmd_sync(args, logger: JsonLogger) -> int:
|
||||
db = _open_db(cfg, logger)
|
||||
try:
|
||||
repo = Repository(db)
|
||||
client = _client(cfg, logger)
|
||||
client = _client(cfg.bilibili.read, logger)
|
||||
service = FolderSyncService(store, repo, client, logger=logger)
|
||||
summary = service.run(full_refresh=args.full)
|
||||
payload = summary.as_dict()
|
||||
@@ -139,7 +148,7 @@ def cmd_snapshot(args, logger: JsonLogger) -> int:
|
||||
try:
|
||||
repo = Repository(db)
|
||||
client = _client(
|
||||
cfg, logger, pacer=Pacer(1.0, 1.6), retries=1
|
||||
cfg.bilibili.read, logger, pacer=Pacer(1.0, 1.6), retries=1
|
||||
)
|
||||
service = SnapshotService(store, repo, client, logger=logger)
|
||||
summary = service.run_if_needed(force=args.force)
|
||||
@@ -153,23 +162,40 @@ def cmd_snapshot(args, logger: JsonLogger) -> int:
|
||||
db.close()
|
||||
|
||||
|
||||
def cmd_auth_check(args, logger: JsonLogger) -> int:
|
||||
store = ConfigStore(args.config, require_auth=False)
|
||||
cfg = store.get()
|
||||
if not cfg.bilibili.cookie:
|
||||
_print({"ok": False, "is_login": False, "message": "未配置 cookie"})
|
||||
return 1
|
||||
client = _client(cfg, logger, retries=1)
|
||||
state = client.check_login()
|
||||
_print({
|
||||
def cmd_auth(args, logger: JsonLogger) -> int:
|
||||
if args.timeout < 1:
|
||||
raise ConfigError("auth --timeout 必须至少为 1 秒")
|
||||
return AuthService(Path(args.config), account=args.account, logger=logger).run(
|
||||
timeout=args.timeout, no_browser=args.no_browser
|
||||
)
|
||||
|
||||
|
||||
def _auth_check(cfg, account: str, logger: JsonLogger) -> dict[str, object]:
|
||||
credentials = getattr(cfg.bilibili, account)
|
||||
if not credentials.cookie:
|
||||
return {"account": account, "ok": False, "is_login": False, "message": "未配置 cookie"}
|
||||
state = _client(credentials, logger, retries=1).check_login()
|
||||
return {
|
||||
"account": account,
|
||||
"ok": state.is_login,
|
||||
"is_login": state.is_login,
|
||||
"mid": state.mid,
|
||||
"uname": state.uname,
|
||||
"code": state.code,
|
||||
"message": state.message,
|
||||
})
|
||||
return 0 if state.is_login else 1
|
||||
}
|
||||
|
||||
|
||||
def cmd_auth_check(args, logger: JsonLogger) -> int:
|
||||
store = ConfigStore(args.config, require_auth=False)
|
||||
cfg = store.get()
|
||||
if args.account != "all":
|
||||
result = _auth_check(cfg, args.account, logger)
|
||||
_print(result)
|
||||
return 0 if result["ok"] else 1
|
||||
results = [_auth_check(cfg, account, logger) for account in ("read", "write")]
|
||||
_print({"ok": all(result["ok"] for result in results), "accounts": results})
|
||||
return 0 if all(result["ok"] for result in results) else 1
|
||||
|
||||
|
||||
def cmd_run(args, logger: JsonLogger) -> int:
|
||||
@@ -179,9 +205,9 @@ def cmd_run(args, logger: JsonLogger) -> int:
|
||||
last: dict[str, object] = {}
|
||||
try:
|
||||
repo = Repository(db)
|
||||
folder_client = _client(cfg, logger)
|
||||
folder_client = _client(cfg.bilibili.read, logger)
|
||||
folder_service = FolderSyncService(store, repo, folder_client, logger=logger)
|
||||
snapshot_client = _client(cfg, logger, pacer=Pacer(1.0, 1.6), retries=1)
|
||||
snapshot_client = _client(cfg.bilibili.read, logger, pacer=Pacer(1.0, 1.6), retries=1)
|
||||
snapshot_service = SnapshotService(store, repo, snapshot_client, logger=logger)
|
||||
|
||||
def run_sync():
|
||||
@@ -284,6 +310,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
return cmd_sync(args, logger)
|
||||
if args.command == "snapshot":
|
||||
return cmd_snapshot(args, logger)
|
||||
if args.command == "auth":
|
||||
return cmd_auth(args, logger)
|
||||
if args.command == "auth-check":
|
||||
return cmd_auth_check(args, logger)
|
||||
if args.command == "run":
|
||||
|
||||
+39
-32
@@ -1,13 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""运行期配置加载、校验与热加载。
|
||||
|
||||
支持两种写法:
|
||||
|
||||
* 新格式:``{"bilibili": {"mid": ..., "cookie": ...}, ...}``;
|
||||
* 旧 ``favsort.py`` 格式:顶层 ``mid`` / ``cookie`` / ``csrf``。
|
||||
|
||||
旧的扁平字段仍然被读取,因此 ``bili-fav auth`` 写回的 config.json 无需迁移即可
|
||||
被看板使用。敏感值只保存在配置对象中,日志输出必须先经过 ``logging_json`` 脱敏。
|
||||
B 站凭据必须采用 ``bilibili.read`` / ``bilibili.write`` 双账号格式。敏感值只
|
||||
保存在配置对象中,日志输出必须先经过 ``logging_json`` 脱敏。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -43,7 +38,10 @@ DEFAULT_HEAT_WEIGHTS: dict[str, float] = {
|
||||
}
|
||||
|
||||
DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"bilibili": {"mid": 0, "csrf": "", "cookie": ""},
|
||||
"bilibili": {
|
||||
"read": {"mid": 0, "csrf": "", "cookie": ""},
|
||||
"write": {"mid": 0, "csrf": "", "cookie": ""},
|
||||
},
|
||||
"telegram": {"enabled": False, "bot_token": "", "chat_id": 0},
|
||||
"web": {
|
||||
"host": "127.0.0.1",
|
||||
@@ -73,9 +71,12 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
||||
|
||||
# 环境变量只作为 CI / 临时调试的覆盖入口,不作为主路径。
|
||||
ENV_OVERRIDES: dict[str, tuple[tuple[str, ...], type]] = {
|
||||
"BILI_COOKIE": (("bilibili", "cookie"), str),
|
||||
"BILI_CSRF": (("bilibili", "csrf"), str),
|
||||
"BILI_MID": (("bilibili", "mid"), int),
|
||||
"BILI_READ_COOKIE": (("bilibili", "read", "cookie"), str),
|
||||
"BILI_READ_CSRF": (("bilibili", "read", "csrf"), str),
|
||||
"BILI_READ_MID": (("bilibili", "read", "mid"), int),
|
||||
"BILI_WRITE_COOKIE": (("bilibili", "write", "cookie"), str),
|
||||
"BILI_WRITE_CSRF": (("bilibili", "write", "csrf"), str),
|
||||
"BILI_WRITE_MID": (("bilibili", "write", "mid"), int),
|
||||
"TG_BOT_TOKEN": (("telegram", "bot_token"), str),
|
||||
"TG_CHAT_ID": (("telegram", "chat_id"), int),
|
||||
"DASHBOARD_AUTH_TOKEN": (("web", "auth_token"), str),
|
||||
@@ -91,12 +92,18 @@ class ConfigError(ValueError):
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BilibiliConfig:
|
||||
class BilibiliCredentials:
|
||||
mid: int
|
||||
csrf: str
|
||||
cookie: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BilibiliConfig:
|
||||
read: BilibiliCredentials
|
||||
write: BilibiliCredentials
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TelegramConfig:
|
||||
enabled: bool
|
||||
@@ -234,18 +241,6 @@ def _as_str_list(value: Any, name: str) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def _legacy_to_current(raw: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""把 favsort 的扁平字段映射到新格式,不修改调用方对象。"""
|
||||
data = copy.deepcopy(dict(raw))
|
||||
if "bilibili" not in data:
|
||||
data["bilibili"] = {
|
||||
"mid": raw.get("mid", 0),
|
||||
"csrf": raw.get("csrf", ""),
|
||||
"cookie": raw.get("cookie", ""),
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
def _validate_folders(value: Any) -> tuple[FolderConfig, ...]:
|
||||
if not isinstance(value, list):
|
||||
raise ConfigError("folders 必须是数组")
|
||||
@@ -273,11 +268,17 @@ def _build_config(data: Mapping[str, Any], path: Path) -> AppConfig:
|
||||
b = data["bilibili"]
|
||||
if not isinstance(b, Mapping):
|
||||
raise ConfigError("bilibili 必须是对象")
|
||||
bilibili = BilibiliConfig(
|
||||
mid=_as_int(b.get("mid", 0), "bilibili.mid", minimum=0),
|
||||
csrf=_as_str(b.get("csrf", "") or "", "bilibili.csrf"),
|
||||
cookie=_as_str(b.get("cookie", "") or "", "bilibili.cookie"),
|
||||
)
|
||||
def credentials(account: str) -> BilibiliCredentials:
|
||||
value = b.get(account)
|
||||
if not isinstance(value, Mapping):
|
||||
raise ConfigError(f"bilibili.{account} 必须是对象")
|
||||
return BilibiliCredentials(
|
||||
mid=_as_int(value.get("mid", 0), f"bilibili.{account}.mid", minimum=0),
|
||||
csrf=_as_str(value.get("csrf", "") or "", f"bilibili.{account}.csrf"),
|
||||
cookie=_as_str(value.get("cookie", "") or "", f"bilibili.{account}.cookie"),
|
||||
)
|
||||
|
||||
bilibili = BilibiliConfig(read=credentials("read"), write=credentials("write"))
|
||||
|
||||
tg = data["telegram"]
|
||||
if not isinstance(tg, Mapping):
|
||||
@@ -431,8 +432,14 @@ def load_config(
|
||||
raise ConfigError(f"无法读取配置文件: {config_path}: {exc}") from exc
|
||||
if not isinstance(raw, Mapping):
|
||||
raise ConfigError("配置文件顶层必须是 JSON 对象")
|
||||
raw_bilibili = raw.get("bilibili")
|
||||
if not isinstance(raw_bilibili, Mapping):
|
||||
raise ConfigError("缺少 bilibili.read / bilibili.write 双账号配置")
|
||||
for account in ("read", "write"):
|
||||
if account not in raw_bilibili:
|
||||
raise ConfigError(f"缺少 bilibili.{account} 账号配置")
|
||||
|
||||
data = _deep_merge(DEFAULT_CONFIG, _legacy_to_current(raw))
|
||||
data = _deep_merge(DEFAULT_CONFIG, raw)
|
||||
|
||||
env_map = os.environ if env is None else env
|
||||
for env_name, (target, caster) in ENV_OVERRIDES.items():
|
||||
@@ -447,9 +454,9 @@ def load_config(
|
||||
_set_path(data, target, value)
|
||||
|
||||
cfg = _build_config(data, config_path)
|
||||
if require_auth and (not cfg.bilibili.cookie or not cfg.bilibili.csrf):
|
||||
if require_auth and (not cfg.bilibili.read.cookie or not cfg.bilibili.read.csrf):
|
||||
raise ConfigError(
|
||||
"config.json 中的 cookie / csrf 不能为空(可先运行 bili-fav auth)"
|
||||
"config.json 中的 bilibili.read.cookie / csrf 不能为空(可先运行 python app.py auth)"
|
||||
)
|
||||
return cfg
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ class FolderSyncService:
|
||||
infos = self.client.fav_infos(
|
||||
folder.media_id,
|
||||
sorted(targets),
|
||||
folder_mid=cfg.bilibili.mid,
|
||||
folder_mid=cfg.bilibili.read.mid,
|
||||
)
|
||||
for aid, resource in infos.items.items():
|
||||
if aid not in remote:
|
||||
|
||||
@@ -255,7 +255,7 @@ class SnapshotService:
|
||||
# ------------------------------------------------------------ internals
|
||||
|
||||
def _check_auth(self, cfg: AppConfig, now: int) -> tuple[bool, str]:
|
||||
if not cfg.bilibili.cookie:
|
||||
if not cfg.bilibili.read.cookie:
|
||||
self.repo.kv_set("auth:last_check", str(now))
|
||||
self.repo.kv_set("auth:ok", "0")
|
||||
self.repo.kv_set("auth:message", "未配置 cookie")
|
||||
|
||||
Reference in new issue
Block a user