Files
huihuiSquare/scripts/batch_replace_avatars.py
2026-07-24 14:04:21 +08:00

129 lines
4.4 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import mimetypes
import os
import sys
import time
from pathlib import Path
import httpx
API_BASE = os.getenv("HUIHUI_API_BASE", "http://192.168.1.188:8000/api").rstrip("/")
AVATAR_DIR = Path(os.getenv("HUIHUI_AVATAR_DIR", str(Path.home() / "Downloads" / "头像")))
ACCOUNT_START = os.getenv("HUIHUI_ACCOUNT_START", "13721560041")
ACCOUNT_END = os.getenv("HUIHUI_ACCOUNT_END", "13721560199")
POLL_SECONDS = int(os.getenv("HUIHUI_POLL_SECONDS", "10"))
TIMEOUT_SECONDS = int(os.getenv("HUIHUI_TIMEOUT_SECONDS", "5400"))
def load_avatar_files(expected_count: int) -> list[Path]:
files = sorted(path for path in AVATAR_DIR.iterdir() if path.is_file())
if len(files) < expected_count:
raise RuntimeError(f"头像文件不足: 需要 {expected_count} 个,实际只有 {len(files)}")
return files[:expected_count]
def fetch_target_users(client: httpx.Client) -> list[dict]:
users: list[dict] = []
page = 1
page_size = 100
while True:
resp = client.get(
f"{API_BASE}/users",
params={"page": page, "page_size": page_size, "keyword": "1372156"},
)
resp.raise_for_status()
payload = resp.json()["data"]
items = payload["items"]
users.extend(items)
if page * page_size >= payload["total"] or not items:
break
page += 1
targets = [u for u in users if ACCOUNT_START <= u["account"] <= ACCOUNT_END]
targets.sort(key=lambda item: item["account"])
return targets
def upload_avatar(client: httpx.Client, user: dict, avatar_path: Path) -> str:
mime = mimetypes.guess_type(str(avatar_path))[0] or "application/octet-stream"
with avatar_path.open("rb") as fp:
resp = client.post(
f"{API_BASE}/users/{user['id']}/upload-avatar",
params={"sync_to_platform": "true"},
files={"file": (avatar_path.name, fp, mime)},
timeout=120,
)
resp.raise_for_status()
payload = resp.json()
if payload.get("code") not in (0, 200):
raise RuntimeError(payload.get("message") or f"上传失败: {payload}")
return payload["data"]["avatar_url"]
def main() -> int:
started_at = time.time()
processed: set[str] = set()
with httpx.Client(timeout=30) as client:
initial_targets = fetch_target_users(client)
if not initial_targets:
raise RuntimeError("没有找到目标手机号范围内的用户")
avatar_files = load_avatar_files(len(initial_targets))
avatar_map = {
user["account"]: avatar_files[idx]
for idx, user in enumerate(initial_targets)
}
print(
f"准备处理 {len(initial_targets)} 个用户,头像目录 {AVATAR_DIR},接口 {API_BASE}",
flush=True,
)
while len(processed) < len(initial_targets):
if time.time() - started_at > TIMEOUT_SECONDS:
pending = sorted(set(avatar_map) - processed)
print(f"超时,仍有 {len(pending)} 个用户未完成: {', '.join(pending[:20])}", flush=True)
return 2
targets = fetch_target_users(client)
status_summary: dict[int, int] = {}
for user in targets:
status_summary[user["status"]] = status_summary.get(user["status"], 0) + 1
for user in targets:
account = user["account"]
if account in processed:
continue
if user["status"] != 2:
continue
avatar_url = upload_avatar(client, user, avatar_map[account])
processed.add(account)
print(
f"[{len(processed):03d}/{len(targets)}] {account} -> {avatar_map[account].name} -> {avatar_url}",
flush=True,
)
if len(processed) < len(initial_targets):
print(
f"等待登录中: 已完成 {len(processed)}/{len(initial_targets)},状态分布 {status_summary}",
flush=True,
)
time.sleep(POLL_SECONDS)
print("全部头像替换完成", flush=True)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except KeyboardInterrupt:
print("已中断", file=sys.stderr)
raise SystemExit(130)