feat(auth): print a scannable ASCII QR in the terminal

WSL and --no-browser never showed the PNG, and placeholder Chinese cookies crashed urllib latin-1 headers before the code was issued.
This commit is contained in:
kairee committed 2026-11-02 10:35:56 +08:00
1 parent 12732f6316
commit 2641ba6f87
5 files changed
+183 -11

No files matched your search

+19 -1
View File
@@ -45,12 +45,30 @@ def strip_identity(cookie: str) -> str:
return "; ".join(kept)
def _is_latin1(value: str) -> bool:
try:
value.encode("latin-1")
except UnicodeEncodeError:
return False
return True
def cookie_header(cookie: str, *, auth: bool = False) -> str:
"""生成可放入 HTTP 头的 Cookie。占位中文、缺 ``=`` 的片段和非 latin-1 字段都会丢掉。"""
source = cookie if auth else strip_identity(cookie)
return "; ".join(
f"{key}={value}"
for key, value in parse_cookie(source)
if _is_latin1(key) and _is_latin1(value)
)
def make_headers(cfg: dict, auth: bool = False, *, referer: str | None = None) -> dict[str, str]:
"""构造与 ``data/raw-request.txt`` 对齐的 B 站 Web 请求头。"""
return {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"cookie": cfg["cookie"] if auth else strip_identity(cfg["cookie"]),
"cookie": cookie_header(str(cfg.get("cookie") or ""), auth=auth),
"dnt": "1",
"origin": "https://space.bilibili.com",
"priority": "u=1, i",
+49 -4
View File
@@ -40,6 +40,47 @@ import qrcodegen # noqa: E402
BASE = "https://api.bilibili.com"
PASSPORT = "https://passport.bilibili.com"
_QR_BLOCKS = {
(False, False): " ",
(True, False): "▀",
(False, True): "▄",
(True, True): "█",
}
_QR_ASCII_STYLE = "\033[48;5;231m\033[38;5;16m"
_QR_ASCII_RESET = "\033[0m"
def render_qr_ascii(text: str, *, border: int = 2) -> str:
"""把登录链接编成终端半块字符二维码(两行模块合成一行)。"""
if border < 0:
raise ValueError("border 必须 >= 0")
qr = qrcodegen.QrCode.encode_text(text, qrcodegen.QrCode.Ecc.MEDIUM)
size = qr.get_size()
dim = size + border * 2
def dark(x: int, y: int) -> bool:
return qr.get_module(x - border, y - border)
lines: list[str] = []
for y in range(0, dim, 2):
row: list[str] = []
bottom_y = y + 1
for x in range(dim):
top = dark(x, y)
bottom = dark(x, bottom_y) if bottom_y < dim else False
row.append(_QR_BLOCKS[(top, bottom)])
lines.append("".join(row))
return "\n".join(lines)
def format_qr_ascii(text: str, *, border: int = 2, color: bool | None = None) -> str:
"""终端二维码;TTY 下加白底黑字,方便深色终端扫码。"""
body = render_qr_ascii(text, border=border)
if color is None:
color = sys.stdout.isatty()
if not color:
return body
return "\n".join(f"{_QR_ASCII_STYLE}{line}{_QR_ASCII_RESET}" for line in body.split("\n"))
def write_qr_png(text: str, path: str | Path, scale: int = 8, border: int = 4) -> Path:
@@ -200,7 +241,7 @@ class AuthService:
def run(self, *, timeout: int, no_browser: bool) -> int:
raw = self._load_raw()
credentials = raw["bilibili"][self.account]
print(f"[auth] 为 {self.account} 账号申请扫码登录 ...")
print(f"[auth] 为 {self.account} 账号申请扫码登录 ...", flush=True)
generated, _ = self._request(
credentials, f"{PASSPORT}/x/passport-login/web/qrcode/generate?source=main-fe-header", auth=False
)
@@ -209,14 +250,18 @@ class AuthService:
if generated.get("code") != 0 or not key or not login_url:
print(f"[auth] 申请二维码失败: code={generated.get('code')} {generated.get('message')}")
return 2
login_url = str(login_url)
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}")
write_qr_png(login_url, qr_path)
print(f"[auth] 二维码已生成: {qr_path}", flush=True)
print("[auth] 请使用哔哩哔哩 App 扫描下方二维码", flush=True)
print(format_qr_ascii(login_url), flush=True)
print(f"[auth] 登录链接: {login_url}", flush=True)
if not no_browser:
try:
open_path(qr_path)
except Exception as exc: # noqa: BLE001
print(f"[auth] 打开图片失败: {exc},请手动打开二维码")
print(f"[auth] 打开图片失败: {exc},请手动扫描终端二维码")
deadline, last = time.time() + timeout, None
while time.time() < deadline:
try: