#!/usr/bin/env python3 """把新版 短剧库.zip 与本地源码对比,展示变更并更新本地旧代码。 用法: python scripts/update_from_zip.py # 从默认地址下载最新包并预览变更 python scripts/update_from_zip.py --apply # 下载并真正更新本地代码 python scripts/update_from_zip.py 某个路径/短剧库.zip # 用本地压缩包 python scripts/update_from_zip.py --url https://... # 指定其他下载地址 行为: 1. 读取压缩包(自动处理中文文件名 GBK/UTF-8、自动剥离压缩包内公共顶层目录) 2. 与本地源码逐文件比对(忽略 .git、data/、日志、压缩包等运行时文件) 3. 列出 新增 / 修改 / 删除 三类变更;加 --apply 后写入本地 4. 更新完成后用 git status / git diff 自查,再提交 """ import argparse import hashlib import sys import tempfile import urllib.parse import urllib.request import zipfile from pathlib import Path DEFAULT_URL = "https://www.252035.xyz/短剧库.zip" REPO_ROOT = Path(__file__).resolve().parent.parent IGNORE_DIRS = {".git", "data", "__pycache__", "dist", "短剧下载"} IGNORE_SUFFIX = {".log", ".zip", ".tar", ".pyc"} IGNORE_NAMES = {".DS_Store", "Thumbs.db"} PROTECT = {"scripts/update_from_zip.py"} # 更新工具自身,永远不删 def download(url: str) -> Path: quoted = urllib.parse.quote(url, safe=":/?&=%") print(f"下载最新压缩包:{url}") request = urllib.request.Request(quoted, headers={"User-Agent": "juku-update/1.0"}) with urllib.request.urlopen(request, timeout=60) as resp: total = int(resp.headers.get("Content-Length") or 0) target = Path(tempfile.gettempdir()) / "短剧库-update.zip" done = 0 with target.open("wb") as f: while True: chunk = resp.read(1 << 16) if not chunk: break f.write(chunk) done += len(chunk) if total: print(f"\r {done}/{total} 字节", end="", flush=True) print(f"\n 已保存到 {target}") return target def norm_name(info: zipfile.ZipInfo) -> str: """修正 zip 内中文文件名。 打包工具有两种常见错误:用 UTF-8 编码但不打 0x800 标志(跨平台工具), 或用 GBK 编码且不打标志(中文 Windows 右键压缩)。依次尝试恢复: UTF-8 优先(其字节序列极少与 GBK 混淆),失败再试 GBK。 """ name = info.filename if not info.flag_bits & 0x800: try: raw = name.encode("cp437") except UnicodeEncodeError: return name.replace("\\", "/") for encoding in ("utf-8", "gbk"): try: name = raw.decode(encoding) break except UnicodeDecodeError: continue return name.replace("\\", "/") def ignored(rel: str) -> bool: parts = rel.split("/") return ( any(p in IGNORE_DIRS for p in parts[:-1]) or Path(rel).name in IGNORE_NAMES or Path(rel).suffix in IGNORE_SUFFIX ) def common_prefix(names: list[str]) -> str: """所有条目共享同一顶层目录时返回该前缀(如 '短剧库/'),否则返回空串。""" if not names: return "" tops = {n.split("/", 1)[0] for n in names} if len(tops) == 1 and all("/" in n for n in names): return next(iter(tops)) + "/" return "" def md5_of(path: Path) -> str: h = hashlib.md5() with path.open("rb") as f: for chunk in iter(lambda: f.read(1 << 16), b""): h.update(chunk) return h.hexdigest() def main() -> int: parser = argparse.ArgumentParser(description="用新版 zip 更新本地短剧库源码") parser.add_argument("zipfile", nargs="?", default=None, help="本地压缩包路径;省略则从 --url 地址下载最新包") parser.add_argument("--url", default=DEFAULT_URL, help=f"新版下载地址(默认 {DEFAULT_URL})") parser.add_argument("--apply", action="store_true", help="真正写入本地(默认只预览)") args = parser.parse_args() if args.zipfile: zip_path = Path(args.zipfile) if not zip_path.exists(): print(f"找不到压缩包:{args.zipfile}") return 1 else: zip_path = download(args.url) with zipfile.ZipFile(zip_path) as zf: infos = [i for i in zf.infolist() if not i.is_dir()] names = [norm_name(i) for i in infos] # 先丢弃 macOS 压缩附带的资源文件,避免干扰顶层目录识别 pairs = [ (info, name) for info, name in zip(infos, names) if "__MACOSX" not in name.split("/") and not Path(name).name.startswith("._") ] prefix = common_prefix([name for _, name in pairs]) if prefix: print(f"检测到压缩包顶层目录:{prefix}(比对时已忽略)") incoming: dict[str, bytes] = {} for info, name in pairs: rel = name[len(prefix):] if prefix else name if rel and not ignored(rel): incoming[rel] = zf.read(info) added, modified, unchanged = [], [], [] for rel, blob in sorted(incoming.items()): local = REPO_ROOT / rel if not local.exists(): added.append(rel) elif md5_of(local) != hashlib.md5(blob).hexdigest(): modified.append(rel) else: unchanged.append(rel) local_files: set[str] = set() for path in REPO_ROOT.rglob("*"): if not path.is_file(): continue rel = path.relative_to(REPO_ROOT).as_posix() if ignored(rel) or rel in IGNORE_NAMES: continue local_files.add(rel) deleted = sorted(local_files - set(incoming) - PROTECT) print(f"\n对比完成:压缩包 {len(incoming)} 个文件,本地 {len(local_files)} 个文件") print(f" 新增 {len(added)} 个") for rel in added: print(f" + {rel}") print(f" 修改 {len(modified)} 个") for rel in modified: print(f" ~ {rel}") print(f" 删除 {len(deleted)} 个") for rel in deleted: print(f" - {rel}") print(f" 未变化 {len(unchanged)} 个") if not (added or modified or deleted): print("本地代码与压缩包一致,无需更新。") return 0 if not args.apply: print("\n当前为预览模式。确认无误后追加 --apply 执行更新,然后 git add -A && git commit。") return 0 for rel in added + modified: target = REPO_ROOT / rel target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(incoming[rel]) print(f"已写入 {rel}") for rel in deleted: (REPO_ROOT / rel).unlink(missing_ok=True) print(f"已删除 {rel}") print("\n更新完成。请执行 git status / git diff 复核后提交。") return 0 if __name__ == "__main__": sys.exit(main())