"""一次性迁移:把 `wallpapers/<游戏>/<壁纸>/` 从「按文件类型分组」改成「一具骨架一组」。 旧: effects/<名>.json ← 骨架数据 images/<名>.atlas images/<名>*.webp ← atlas 与页(扁平) images/<名>/<名>.atlas + <名>.png ← 另一档用的「一骨架一目录」 images/scene/<图> ← 场景图 images/ava.jpg ← 背景图 新: spines/<名>/<名>.json | <名>.atlas | <页…> scene/<图> ← 场景图与背景图 audios/… ← 不动 规则(页的判定用 **atlas 自己声明的页名**,与运行时同一份真相,不做名字启发式): 1. 每个 `` 的 stem 建 `spines//`,把该 atlas 与它声明的页搬进去; 2. `effects/*.json` → `spines//.json`; 3. `images/` 里剩下的文件(含 `images/scene/*`)→ `scene/`; 4. 按实际搬动结果重写 `preset.template.json` 里所有 `./…` 路径。 用法: python .scratch/migrate-walls.py # dry-run,只打印 python .scratch/migrate-walls.py --apply # 真搬 + 改预设 """ from __future__ import annotations import json import re import shutil import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] WALLS = ROOT / "wallpapers" APPLY = "--apply" in sys.argv PAGE_EXT = re.compile(r"\.(png|webp|jpg|jpeg)$", re.IGNORECASE) def atlas_pages(text: str) -> list[str]: """取 atlas 声明的页名(非属性行、以图片扩展名结尾的行)。""" pages = [] for raw in text.splitlines(): line = raw.strip() if line and ":" not in line and PAGE_EXT.search(line): pages.append(line) return pages def migrate(wall: Path) -> dict[str, str]: """搬一档壁纸,返回 {旧相对路径: 新相对路径}(相对壁纸目录,不带 ./)。""" moves: dict[str, str] = {} # 1) atlas 及其声明的页 atlases = list((wall / "images").rglob("*.atlas")) + list((wall / "spines").rglob("*.atlas")) for atlas in atlases: stem = atlas.stem dest_dir = wall / "spines" / stem pages = atlas_pages(atlas.read_text(encoding="utf-8")) moves[str(atlas.relative_to(wall))] = f"spines/{stem}/{atlas.name}" for page in pages: src = atlas.parent / page if src.exists(): moves[str(src.relative_to(wall))] = f"spines/{stem}/{page}" # 2) 骨架 json for js in (wall / "effects").glob("*.json") if (wall / "effects").is_dir() else []: stem = js.stem moves[str(js.relative_to(wall))] = f"spines/{stem}/{js.name}" # 3) images/ 下剩下的(含 images/scene/*)→ scene/ if (wall / "images").is_dir(): for path in sorted((wall / "images").rglob("*")): if path.is_dir(): continue rel = str(path.relative_to(wall)) if rel in moves: continue moves[rel] = f"scene/{path.name}" # 4) 重写预设(按映射精确替换 ./ 路径) preset_path = wall / "preset.template.json" preset = json.loads(preset_path.read_text(encoding="utf-8")) replaced: list[str] = [] def rewrite(value: object) -> object: if isinstance(value, str) and value.startswith("./"): old = value[2:] new = moves.get(old) if new: replaced.append(f"{value} → ./{new}") return f"./{new}" if isinstance(value, dict): return {k: rewrite(v) for k, v in value.items()} if isinstance(value, list): return [rewrite(v) for v in value] return value new_preset = rewrite(preset) print(f"\n== {wall.relative_to(ROOT)}") for old, new in sorted(moves.items()): print(f" {old:52} → {new}") for line in replaced: print(f" 预设: {line}") if APPLY: for old, new in moves.items(): src, dst = wall / old, wall / new dst.parent.mkdir(parents=True, exist_ok=True) shutil.move(str(src), str(dst)) # 清掉空目录 for empty in ["effects", "images"]: d = wall / empty if d.is_dir(): shutil.rmtree(d, ignore_errors=True) preset_path.write_text(json.dumps(new_preset, ensure_ascii=False, indent="\t") + "\n", encoding="utf-8") return moves total = 0 for wall in sorted(p for p in WALLS.rglob("*") if (p / "preset.template.json").is_file()): total += len(migrate(wall)) print(f"\n{'已搬动' if APPLY else '将搬动'} {total} 个文件;{'(dry-run,加 --apply 才真搬)' if not APPLY else ''}")