#!/usr/bin/env python3 """图库迁移(Phase 2 T3):Halo photos → content/photos/{jpg,json sidecar} EXIF 不在迁移时提——构建期 photos loader(src/lib/exif.ts)会补全; sidecar 里 exif 留空对象。分组:Halo displayName 含山/雪/land/snow → landscape,否则 street(当前仅一组 snow mountain → landscape)。 """ import json import re import urllib.request from pathlib import Path from urllib.parse import unquote BASE = "http://127.0.0.1:18090" OUT = Path("content/photos") def slugify(name: str) -> str: stem = re.sub(r"\.(jpg|jpeg|png|webp)$", "", name, flags=re.I) stem = re.sub(r"[^0-9A-Za-z\u4e00-\u9fff-]+", "-", stem).strip("-") return stem or "photo" def group_of(display: str) -> str: low = display.lower() return "landscape" if any(k in low for k in ("山", "雪", "snow", "land", "mountain")) else "street" def compress(data: bytes) -> bytes: from io import BytesIO from PIL import Image img = Image.open(BytesIO(data)) # EXIF 必须显式带上:PIL 重存默认剥离,而构建期 exifr 正是从文件提 EXIF exif = img.info.get("exif") if img.mode not in ("RGB", "L"): img = img.convert("RGB") img.thumbnail((2400, 2400)) buf = BytesIO() # PIL 的 JPEG 保存不接受 exif=None(len(None) 崩),仅在确有 EXIF 时传参 kwargs = {"exif": exif} if exif else {} img.save(buf, "JPEG", quality=86, optimize=True, **kwargs) return buf.getvalue() def main() -> int: # 真实照片取代样例:先清样例(jpg/json 成对;仅清本脚本管辖的产物) for f in OUT.glob("*"): if f.suffix in (".jpg", ".json"): f.unlink() photos = json.load(open(".migration/photos.json", encoding="utf-8")) n = 0 for p in photos: spec = p["spec"] url = spec.get("url") or spec.get("cover") or "" if not url.startswith("/upload/"): print(f" skip 非本站图: {url[:60]}") continue fname = unquote(url.rsplit("/", 1)[1]) slug = slugify(spec.get("displayName") or fname) data = urllib.request.urlopen(BASE + url, timeout=60).read() # 原图双留档:仓外 originals store(构建时独立 rsync,服务"查看原图/ # 下载")+ Phase 0 备份 tar;git 仓只进压缩版(196M 事故教训) STORE = Path.home() / "docker_apps/blog/photos-originals" STORE.mkdir(parents=True, exist_ok=True) (STORE / f"{slug}.jpg").write_bytes(data) (OUT / f"{slug}.jpg").write_bytes(compress(data)) (OUT / f"{slug}.json").write_text(json.dumps({ "title": spec.get("displayName", fname).rsplit(".", 1)[0], "image": f"./{slug}.jpg", "group": group_of(spec.get("groupName", "")), "alt": spec.get("description", ""), "exif": {}, }, ensure_ascii=False, indent=1)) n += 1 print(f"migrated {n} photos") return 0 if n else 1 if __name__ == "__main__": raise SystemExit(main())