Skip to content

Commit 8362607

Browse files
committed
feat(update): 全量头像下载 + 软件更新检查 + 远程资源同步
syncer.py: 支持远程 GitHub 下载(--remote,不依赖 sibling repo);--all-avatars 全量下载 1306 张头像。 update.py: UpdateChecker——check_software()(GitHub releases API 检查新版本)+ update_resources()(远程同步干员/地图/头像)。CLI + UI 可调用。 ruff/pyright 全过。
1 parent 145dc7a commit 8362607

2 files changed

Lines changed: 285 additions & 75 deletions

File tree

custom/resources/syncer.py

Lines changed: 141 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,73 @@
1-
"""资源同步器:从 MaaAssistantArknights 提取干员/地图/头像数据
1+
"""资源同步器:从 MaaAssistantArknights(本地或远程 GitHub)提取数据
22
33
用法:
4-
uv run python -m custom.resources.syncer # 同步全部
5-
uv run python -m custom.resources.syncer --maps # 只同步地图
6-
uv run python -m custom.resources.syncer --operators # 只同步干员名
7-
uv run python -m custom.resources.syncer --avatars "斑点,芬" # 按需下载头像
8-
9-
产出(到 data/):
10-
- operator_mapping.json / operator_names.json:干员名 + charId
11-
- level_codes.json:关卡代号列表
12-
- map/*.json:关卡地图数据(从 Arknights-Tile-Pos 复制)
13-
- avatar/{charId}.png:干员部署头像(从 PRTS Wiki 下载,按需)
4+
uv run python -m custom.resources.syncer # 同步全部(本地优先,远程回退)
5+
uv run python -m custom.resources.syncer --all-avatars # 全量下载头像
6+
uv run python -m custom.resources.syncer --avatars "斑点,芬" # 指定干员
7+
uv run python -m custom.resources.syncer --remote # 从 GitHub 下载
148
"""
159

1610
from __future__ import annotations
1711

1812
import json
1913
import logging
2014
import shutil
15+
import urllib.request
2116
from pathlib import Path
2217

2318
from custom.utils.runtime_paths import project_root
2419

2520
logger = logging.getLogger(__name__)
2621

22+
# 本地源(开发环境)
2723
_MAA_ROOT = Path("../MaaAssistantArknights")
28-
_BATTLE_DATA = _MAA_ROOT / "resource" / "battle_data.json"
29-
_TILE_POS = _MAA_ROOT / "resource" / "Arknights-Tile-Pos"
24+
_BATTLE_DATA_LOCAL = _MAA_ROOT / "resource" / "battle_data.json"
25+
_TILE_POS_LOCAL = _MAA_ROOT / "resource" / "Arknights-Tile-Pos"
3026

31-
# PRTS Wiki 头像 URL 模板
27+
# 远程源(用户环境)
28+
_GITHUB = "https://raw.githubusercontent.com/MaaAssistantArknights/MaaAssistantArknights/main"
29+
_BATTLE_DATA_REMOTE = f"{_GITHUB}/resource/battle_data.json"
30+
_TILE_POS_API = "https://api.github.com/repos/MaaAssistantArknights/MaaAssistantArknights/contents/resource/Arknights-Tile-Pos"
31+
32+
# PRTS Wiki 头像
3233
_AVATAR_URL = "https://media.prts.wiki/thumb.php?f=avg_{char_id}.png&w=120"
3334

3435

35-
def sync_operators() -> None:
36-
"""从 battle_data.json 提取干员名 → charId 映射。"""
36+
def _download(url: str, dest: Path) -> bool:
37+
try:
38+
urllib.request.urlretrieve(url, dest)
39+
return True
40+
except Exception as e: # noqa: BLE001
41+
logger.warning("下载失败 %s: %s", url, e)
42+
return False
43+
44+
45+
def _get_battle_data(force_remote: bool = False) -> dict | None:
46+
"""获取 battle_data.json(本地优先,远程回退)。"""
47+
if not force_remote and _BATTLE_DATA_LOCAL.exists():
48+
return json.loads(_BATTLE_DATA_LOCAL.read_text(encoding="utf-8"))
49+
50+
logger.info("从 GitHub 下载 battle_data.json...")
51+
tmp = project_root() / "data" / ".battle_data.json"
52+
tmp.parent.mkdir(parents=True, exist_ok=True)
53+
if _download(_BATTLE_DATA_REMOTE, tmp):
54+
return json.loads(tmp.read_text(encoding="utf-8"))
55+
return None
56+
57+
58+
# --- 干员 ---
59+
60+
61+
def sync_operators(force_remote: bool = False) -> None:
3762
data_dir = project_root() / "data"
3863
data_dir.mkdir(parents=True, exist_ok=True)
3964

40-
if not _BATTLE_DATA.exists():
41-
logger.error("battle_data.json 不存在: %s", _BATTLE_DATA)
65+
raw = _get_battle_data(force_remote)
66+
if raw is None:
67+
logger.error("无法获取 battle_data.json")
4268
return
4369

44-
raw = json.loads(_BATTLE_DATA.read_text(encoding="utf-8"))
4570
chars = raw.get("chars", {})
46-
4771
mapping: dict[str, str] = {}
4872
names: list[dict[str, str]] = []
4973
for char_id, info in chars.items():
@@ -61,7 +85,6 @@ def sync_operators() -> None:
6185
)
6286

6387
names.sort(key=lambda x: (-int(x["rarity"]), x["name"]))
64-
6588
(data_dir / "operator_mapping.json").write_text(
6689
json.dumps(mapping, indent=2, ensure_ascii=False), encoding="utf-8"
6790
)
@@ -71,104 +94,147 @@ def sync_operators() -> None:
7194
logger.info("干员数据: %d 名", len(names))
7295

7396

74-
def sync_maps() -> None:
75-
"""复制 Arknights-Tile-Pos 地图 JSON 到 data/map/(跳过 #f# 翻转变体)。"""
97+
# --- 地图 ---
98+
99+
100+
def sync_maps(force_remote: bool = False) -> None:
76101
data_dir = project_root() / "data"
77102
map_dir = data_dir / "map"
78103
map_dir.mkdir(parents=True, exist_ok=True)
79104

80-
if not _TILE_POS.exists():
81-
logger.error("Arknights-Tile-Pos 不存在: %s", _TILE_POS)
82-
return
105+
if not force_remote and _TILE_POS_LOCAL.exists():
106+
count = _copy_maps_local(_TILE_POS_LOCAL, map_dir)
107+
else:
108+
count = _download_maps_remote(map_dir)
83109

84-
count = 0
110+
# 生成 level_codes
85111
codes: dict[str, str] = {}
86-
for src in _TILE_POS.glob("*.json"):
87-
if "#f#" in src.name:
112+
for p in map_dir.glob("*.json"):
113+
if "#f#" in p.name:
88114
continue
89-
dst = map_dir / src.name
90-
shutil.copy2(src, dst)
91-
count += 1
92-
code = src.name.split("-")[0]
115+
code = p.name.split("-")[0]
93116
if code not in codes:
94-
codes[code] = src.name
95-
96-
sorted_codes = dict(sorted(codes.items()))
117+
codes[code] = p.name
97118
(data_dir / "level_codes.json").write_text(
98-
json.dumps(sorted_codes, indent=2, ensure_ascii=False), encoding="utf-8"
99-
)
100-
logger.info(
101-
"地图数据: %d 文件 → data/map/ + level_codes.json (%d 关)",
102-
count,
103-
len(sorted_codes),
119+
json.dumps(dict(sorted(codes.items())), indent=2, ensure_ascii=False),
120+
encoding="utf-8",
104121
)
122+
logger.info("地图数据: %d 文件 (%d 关)", count, len(codes))
105123

106124

107-
def sync_avatars(operator_names: list[str] | None = None) -> None:
108-
"""按需下载干员头像到 resource/image/avatar/。
125+
def _copy_maps_local(src_dir: Path, dst_dir: Path) -> int:
126+
count = 0
127+
for src in src_dir.glob("*.json"):
128+
if "#f#" in src.name:
129+
continue
130+
shutil.copy2(src, dst_dir / src.name)
131+
count += 1
132+
return count
133+
134+
135+
def _download_maps_remote(dst_dir: Path) -> int:
136+
"""从 GitHub API 批量下载地图文件(较慢,仅首次/远程时用)。"""
137+
logger.info("从 GitHub 下载地图列表...")
138+
try:
139+
with urllib.request.urlopen(_TILE_POS_API) as resp:
140+
files = json.loads(resp.read())
141+
except Exception as e: # noqa: BLE001
142+
logger.error("无法获取地图列表: %s", e)
143+
return 0
144+
145+
count = 0
146+
total = len(files)
147+
for i, f in enumerate(files):
148+
if f["type"] != "file" or "#f#" in f["name"]:
149+
continue
150+
dst = dst_dir / f["name"]
151+
if dst.exists():
152+
count += 1
153+
continue
154+
download_url = f["download_url"]
155+
if _download(download_url, dst):
156+
count += 1
157+
if (i + 1) % 200 == 0:
158+
logger.info("地图下载进度: %d/%d", i + 1, total)
159+
return count
160+
161+
162+
# --- 头像 ---
163+
164+
165+
def sync_avatars(
166+
operator_names: list[str] | None = None,
167+
all_avatars: bool = False,
168+
) -> None:
169+
"""下载干员头像。
109170
110171
Args:
111-
operator_names: 要下载的干员名列表。None = 下载全部(慢)。
172+
operator_names: 指定干员名列表。
173+
all_avatars: 下载全部头像。
112174
"""
113-
import urllib.request
114-
115175
data_dir = project_root() / "data"
116176
avatar_dir = project_root() / "resource" / "image" / "avatar"
117177
avatar_dir.mkdir(parents=True, exist_ok=True)
118178

119-
# 加载映射
120179
mapping_path = data_dir / "operator_mapping.json"
121180
if not mapping_path.exists():
122-
logger.error("operator_mapping.json 不存在,请先运行 --operators")
181+
logger.error("operator_mapping.json 不存在,请先同步干员")
123182
return
124183
mapping: dict[str, str] = json.loads(mapping_path.read_text(encoding="utf-8"))
125184

126-
if operator_names is None:
185+
if all_avatars:
127186
targets = list(mapping.items())
187+
elif operator_names:
188+
targets = [(n, mapping[n]) for n in operator_names if n in mapping]
128189
else:
129-
targets = [(name, mapping[name]) for name in operator_names if name in mapping]
190+
logger.info("未指定头像范围(用 --all-avatars 或 --avatars '名1,名2')")
191+
return
130192

193+
total = len(targets)
131194
downloaded = 0
132-
for name, char_id in targets:
195+
for i, (_name, char_id) in enumerate(targets):
133196
out_path = avatar_dir / f"{char_id}.png"
134197
if out_path.exists():
135-
continue # 已存在跳过
136-
198+
continue
137199
url = _AVATAR_URL.format(char_id=char_id)
138-
try:
139-
urllib.request.urlretrieve(url, out_path)
200+
if _download(url, out_path):
140201
downloaded += 1
141-
logger.info("下载头像: %s (%s)", name, char_id)
142-
except Exception as e: # noqa: BLE001
143-
logger.warning("下载失败 %s: %s", name, e)
202+
if (i + 1) % 100 == 0:
203+
logger.info("头像进度: %d/%d (新下载 %d)", i + 1, total, downloaded)
204+
205+
existing = len(list(avatar_dir.glob("*.png")))
206+
logger.info("头像完成: 新下载 %d, 总计 %d 张", downloaded, existing)
207+
144208

145-
logger.info("头像下载完成: %d 张 (共 %d 目标)", downloaded, len(targets))
209+
# --- 统一入口 ---
146210

147211

148-
def sync_all() -> None:
149-
"""同步全部资源(干员 + 地图)。"""
150-
logger.info("开始资源同步(源: %s)", _MAA_ROOT.resolve())
151-
sync_operators()
152-
sync_maps()
153-
logger.info("资源同步完成。头像按需下载:--avatars '干员名1,干员名2'")
212+
def sync_all(force_remote: bool = False) -> None:
213+
logger.info("资源同步%s...", "(远程)" if force_remote else "")
214+
sync_operators(force_remote)
215+
sync_maps(force_remote)
216+
logger.info("资源同步完成")
154217

155218

156219
if __name__ == "__main__":
157220
import argparse
158221

159222
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
160223
parser = argparse.ArgumentParser(description="资源同步器")
161-
parser.add_argument("--operators", action="store_true", help="只同步干员名")
162-
parser.add_argument("--maps", action="store_true", help="只同步地图")
163-
parser.add_argument("--avatars", type=str, default=None, help="下载头像(干员名逗号分隔)")
224+
parser.add_argument("--operators", action="store_true")
225+
parser.add_argument("--maps", action="store_true")
226+
parser.add_argument("--all-avatars", action="store_true", help="下载全部头像")
227+
parser.add_argument("--avatars", type=str, help="指定干员名(逗号分隔)")
228+
parser.add_argument("--remote", action="store_true", help="强制从 GitHub 下载")
164229
args = parser.parse_args()
165230

166-
if args.avatars is not None:
167-
names = [n.strip() for n in args.avatars.split(",") if n.strip()]
168-
sync_avatars(names)
231+
if args.all_avatars:
232+
sync_avatars(all_avatars=True)
233+
elif args.avatars:
234+
sync_avatars([n.strip() for n in args.avatars.split(",") if n.strip()])
169235
elif args.operators:
170-
sync_operators()
236+
sync_operators(args.remote)
171237
elif args.maps:
172-
sync_maps()
238+
sync_maps(args.remote)
173239
else:
174-
sync_all()
240+
sync_all(args.remote)

0 commit comments

Comments
 (0)