Files
blog/scripts/migrate/photos_migrate.py
T
wait4xandGLM-5.3 382e56431f fix(photos): 入库压缩保留 EXIF 并补未来上传的压缩缺口
- 迁移器与新增 photos_ingest.py 压缩时显式 exif= 传参(PIL 重存默认
  剥离——此前 21 张已全部丢 EXIF,图库参数条构建期数据源被抽空)
- exif=None 会使 PIL JPEG 保存 len(None) 崩,改条件传参
- photos_ingest.py:幂等入库处理器(长边 2400/q86,>1.5M 才动),
  堵住 gitea/本地上传原图直入仓的缺口(196M 事故成因)
- 重跑迁移:19/21 带 EXIF(2 张原图本无),体积 9.7M

验证: pnpm astro build 11 页 exit 0;EXIF 保留计数 19/21

Spec: none (Phase 2 归档后的独立小修:EXIF 修复 + ingest 补缺)

Co-authored-by: GLM-5.3
2026-10-25 09:01:07 +08:00

83 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()
# 入仓前压到长边 2400/q86:原图总计 196M(单张最大 28M)会永久膨胀
# 公开仓历史并撞死 nginx 100M 推送上限;原图已随 Phase 0 备份归档,
# 站内展示由 sharp 出 srcset,2400px 足够 lightbox 全屏
(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())