maipulse: 脱敏存档
- Python 后端 + 单页前端 + Android(Chaquopy) 壳 - 曲库 1343 首 / 称号 2800 个 - 机台身份与店铺信息已抹除,字段留空 - 打包脚本含 16KB 页对齐与自动脱敏
This commit is contained in:
commit
8421898626
161 files changed
+50297
No files matched your search
@@ -0,0 +1,258 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""把 SDGB 官方更新包(A0xx)里的曲目 / 称号合并进 maipulse 的 master 数据。
|
||||
|
||||
用法:
|
||||
python tools/apply_update.py "C:\\Users\\20558\\Downloads\\SDGB_A041_20261020134418_0"
|
||||
python tools/apply_update.py <包目录> --dry 只看差异,不写盘
|
||||
|
||||
包里有什么就用什么:
|
||||
music/music*/Music.xml -> data/master/music.json(定数、bpm、版本、谱师全在这)
|
||||
title/title*/Title.xml -> data/master/title.json
|
||||
其它目录(event / map / information / MovieData / SoundData / AssetBundleImages)是机台资源,
|
||||
maipulse 用不到,直接跳过。
|
||||
|
||||
★ 只增量合并:已存在的 id 用包里更完整的字段覆盖空值,不删任何旧条目。
|
||||
★ 写盘前自动备份到 data/_backup/<时间戳>/。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "data")
|
||||
MASTER_DIR = os.path.join(DATA_DIR, "master")
|
||||
MASTER_INDEX = os.path.join(DATA_DIR, "master.json")
|
||||
BACKUP_ROOT = os.path.join(DATA_DIR, "_backup")
|
||||
|
||||
|
||||
def txt(node, path, default=""):
|
||||
e = node.find(path)
|
||||
if e is None:
|
||||
return default
|
||||
return (e.text or default) or default
|
||||
|
||||
|
||||
def num(node, path, default=0):
|
||||
e = node.find(path)
|
||||
if e is None:
|
||||
return default
|
||||
try:
|
||||
return int((e.text or "").strip())
|
||||
except (ValueError, AttributeError):
|
||||
return default
|
||||
|
||||
|
||||
def flag(node, path, default=False):
|
||||
return (txt(node, path, "true" if default else "false") or "").strip().lower() == "true"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 解析
|
||||
|
||||
def parse_music(path: str) -> dict:
|
||||
root = ET.parse(path).getroot()
|
||||
notes = []
|
||||
for n in root.findall("./notesData/Notes"):
|
||||
if not flag(n, "./isEnable", True):
|
||||
notes.append({"level": 0, "levelDecimal": 0, "designer": ""})
|
||||
continue
|
||||
notes.append({
|
||||
"level": num(n, "./level"),
|
||||
"levelDecimal": num(n, "./levelDecimal"),
|
||||
"designer": txt(n, "./notesDesigner/str"),
|
||||
})
|
||||
# 谱面固定补到 6 档(BAS/ADV/EXP/MAS/Re:MAS + 宴),和现有 master 对齐
|
||||
while len(notes) < 6:
|
||||
notes.append({"level": 0, "levelDecimal": 0, "designer": ""})
|
||||
return {
|
||||
"id": num(root, "./name/id"),
|
||||
"name": txt(root, "./name/str"),
|
||||
"sortName": txt(root, "./sortName"),
|
||||
"artist": txt(root, "./artistName/str"),
|
||||
"genre": txt(root, "./genreName/str"),
|
||||
"bpm": num(root, "./bpm"),
|
||||
"version": num(root, "./version"),
|
||||
"addVersion": txt(root, "./AddVersion/str"),
|
||||
"lockType": num(root, "./lockType"),
|
||||
"disable": flag(root, "./disable"),
|
||||
"notes": notes,
|
||||
}
|
||||
|
||||
|
||||
def parse_title(path: str) -> dict:
|
||||
root = ET.parse(path).getroot()
|
||||
return {
|
||||
"id": num(root, "./name/id"),
|
||||
"name": txt(root, "./name/str"),
|
||||
# Title.xml 里叫 <genre>,Music.xml 里叫 <genreName>,两个都兼容
|
||||
"genre": txt(root, "./genre/str") or txt(root, "./genreName/str"),
|
||||
"release": txt(root, "./releaseTagName/str"),
|
||||
"lock": txt(root, "./dispCond"),
|
||||
"norm": txt(root, "./normText"),
|
||||
"isDefault": flag(root, "./isDefault"),
|
||||
"disable": flag(root, "./disable"),
|
||||
}
|
||||
|
||||
|
||||
def collect(pkg: str, sub: str, filename: str, parser) -> list:
|
||||
out = []
|
||||
base = os.path.join(pkg, sub)
|
||||
if not os.path.isdir(base):
|
||||
return out
|
||||
for name in sorted(os.listdir(base)):
|
||||
p = os.path.join(base, name, filename)
|
||||
if os.path.isfile(p):
|
||||
try:
|
||||
item = parser(p)
|
||||
except ET.ParseError as exc:
|
||||
print(" 跳过 %s:%s" % (p, exc))
|
||||
continue
|
||||
if item.get("id"):
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 合并
|
||||
|
||||
def merge(old: list, new: list):
|
||||
"""按 id 增量合并。返回 (合并后列表, 新增, 更新)。"""
|
||||
index = {x["id"]: i for i, x in enumerate(old)}
|
||||
added, updated = [], []
|
||||
for item in new:
|
||||
i = index.get(item["id"])
|
||||
if i is None:
|
||||
old.append(item)
|
||||
index[item["id"]] = len(old) - 1
|
||||
added.append(item)
|
||||
continue
|
||||
cur = old[i]
|
||||
dirty = False
|
||||
for k, v in item.items():
|
||||
if k == "notes":
|
||||
# 谱面:包里启用的档位比旧的更权威
|
||||
if any(n["level"] or n["levelDecimal"] for n in v):
|
||||
if v != cur.get("notes"):
|
||||
cur["notes"] = v
|
||||
dirty = True
|
||||
continue
|
||||
if v in ("", 0, False, None) and cur.get(k) not in ("", 0, False, None):
|
||||
continue # 包里是空值就不覆盖已有值
|
||||
if cur.get(k) != v:
|
||||
cur[k] = v
|
||||
dirty = True
|
||||
if dirty:
|
||||
updated.append(item["id"])
|
||||
old.sort(key=lambda x: x["id"])
|
||||
return old, added, updated
|
||||
|
||||
|
||||
def backup(paths: list) -> str:
|
||||
stamp = time.strftime("%Y%m%d-%H%M%S")
|
||||
dst = os.path.join(BACKUP_ROOT, stamp)
|
||||
os.makedirs(dst, exist_ok=True)
|
||||
for p in paths:
|
||||
if os.path.isfile(p):
|
||||
shutil.copy2(p, os.path.join(dst, os.path.basename(p)))
|
||||
return dst
|
||||
|
||||
|
||||
def write_json(path: str, obj):
|
||||
with io.open(path, "w", encoding="utf-8", newline="\n") as fp:
|
||||
json.dump(obj, fp, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def rebuild_index():
|
||||
"""master.json 是总索引:{kind: {kind, count, items}}。"""
|
||||
index = {}
|
||||
if os.path.isfile(MASTER_INDEX):
|
||||
try:
|
||||
index = json.load(io.open(MASTER_INDEX, encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
index = {}
|
||||
for name in os.listdir(MASTER_DIR):
|
||||
if not name.endswith(".json"):
|
||||
continue
|
||||
kind = name[:-5]
|
||||
p = os.path.join(MASTER_DIR, name)
|
||||
try:
|
||||
items = json.load(io.open(p, encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
index[kind] = {"kind": kind, "count": len(items), "items": items}
|
||||
write_json(MASTER_INDEX, index)
|
||||
return {k: v["count"] for k, v in index.items()}
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
if not args:
|
||||
print(__doc__)
|
||||
return 1
|
||||
pkg = os.path.abspath(args[0])
|
||||
dry = "--dry" in sys.argv
|
||||
if not os.path.isdir(pkg):
|
||||
print("包目录不存在:%s" % pkg)
|
||||
return 1
|
||||
|
||||
print("包:%s" % pkg)
|
||||
ver = os.path.join(pkg, "DataConfig.xml")
|
||||
if os.path.isfile(ver):
|
||||
try:
|
||||
r = ET.parse(ver).getroot()
|
||||
print("版本:%s.%s.%s" % (num(r, "./version/major"), num(r, "./version/minor"),
|
||||
num(r, "./version/release")))
|
||||
except ET.ParseError:
|
||||
pass
|
||||
|
||||
music_path = os.path.join(MASTER_DIR, "music.json")
|
||||
title_path = os.path.join(MASTER_DIR, "title.json")
|
||||
music_old = json.load(io.open(music_path, encoding="utf-8"))
|
||||
title_old = json.load(io.open(title_path, encoding="utf-8"))
|
||||
|
||||
pkg_music = collect(pkg, "music", "Music.xml", parse_music)
|
||||
pkg_title = collect(pkg, "title", "Title.xml", parse_title)
|
||||
print("包里:曲目 %d 首 / 称号 %d 个" % (len(pkg_music), len(pkg_title)))
|
||||
|
||||
music_new, m_add, m_upd = merge(music_old, pkg_music)
|
||||
title_new, t_add, t_upd = merge(title_old, pkg_title)
|
||||
|
||||
print("\n曲目新增 %d 首:" % len(m_add))
|
||||
for it in m_add:
|
||||
lv = "/".join("%d%s" % (n["level"], ("." + str(n["levelDecimal"])) if n["levelDecimal"] else "")
|
||||
for n in it["notes"][:4])
|
||||
print(" %-7d %-30s v%-6d %s" % (it["id"], it["name"][:28], it["version"], lv))
|
||||
if m_upd:
|
||||
print("曲目更新 %d 首:%s" % (len(m_upd), ", ".join(map(str, m_upd))))
|
||||
|
||||
print("\n称号新增 %d 个:" % len(t_add))
|
||||
for it in t_add:
|
||||
print(" %-7d %s" % (it["id"], it["name"]))
|
||||
if t_upd:
|
||||
print("称号更新 %d 个:%s" % (len(t_upd), ", ".join(map(str, t_upd))))
|
||||
|
||||
print("\n总计:曲目 %d -> %d,称号 %d -> %d"
|
||||
% (len(music_old) - len(m_add), len(music_new),
|
||||
len(title_old) - len(t_add), len(title_new)))
|
||||
|
||||
if dry:
|
||||
print("\n--dry:没有写盘")
|
||||
return 0
|
||||
|
||||
dst = backup([music_path, title_path, MASTER_INDEX])
|
||||
print("\n已备份到 %s" % dst)
|
||||
write_json(music_path, music_new)
|
||||
write_json(title_path, title_new)
|
||||
counts = rebuild_index()
|
||||
print("已写入。master.json 计数:")
|
||||
for k, v in sorted(counts.items()):
|
||||
print(" %-8s %d" % (k, v))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user