#!/usr/bin/env python3 """Validate a media-assets bundle directory before packaging or release.""" from __future__ import annotations import json import sys from pathlib import Path LFS_POINTER_PREFIX = b"version https://git-lfs.github.com/spec/v1" MIN_MP3_BYTES = 100 * 1024 def _fail(message: str) -> None: print(f"ERROR: {message}", file=sys.stderr) sys.exit(1) def _is_lfs_pointer(path: Path) -> bool: with path.open("rb") as handle: header = handle.read(256) return header.startswith(LFS_POINTER_PREFIX) def _require_path(root: Path, relative: str, *, must_be_dir: bool = False) -> Path: target = root / relative if not target.exists(): _fail(f"missing required path: {relative}") if must_be_dir and not target.is_dir(): _fail(f"expected directory: {relative}") if not must_be_dir and not target.is_file(): _fail(f"expected file: {relative}") return target def _validate_manifest(manifest_path: Path) -> None: try: data = json.loads(manifest_path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: _fail(f"manifest.json is not valid JSON: {exc}") try: win32 = data["tools"]["ffmpeg"]["win32-x64"] urls = win32["urls"] archive_paths = win32["archive_paths"] except (KeyError, TypeError) as exc: _fail(f"manifest.json missing required tools.ffmpeg.win32-x64 fields: {exc}") if not isinstance(urls, list) or not urls: _fail("manifest.json tools.ffmpeg.win32-x64.urls must be a non-empty array") for key in ("ffmpeg", "ffprobe"): if key not in archive_paths or not archive_paths[key]: _fail(f"manifest.json missing tools.ffmpeg.win32-x64.archive_paths.{key}") def validate_bundle(root: Path) -> list[tuple[Path, int]]: root = root.resolve() if not root.is_dir(): _fail(f"bundle root is not a directory: {root}") _require_path(root, "README.md") manifest_path = _require_path(root, "manifest.json") _require_path(root, "music", must_be_dir=True) _require_path(root, "fonts", must_be_dir=True) _require_path(root, "watermark", must_be_dir=True) mp3_files = sorted(root.joinpath("music").rglob("*.mp3")) if not mp3_files: _fail("music/ must contain at least one .mp3 file") mp3_stats: list[tuple[Path, int]] = [] for mp3_path in mp3_files: size = mp3_path.stat().st_size if _is_lfs_pointer(mp3_path): rel = mp3_path.relative_to(root) _fail(f"{rel} is a Git LFS pointer, not a real mp3 file") if size <= MIN_MP3_BYTES: rel = mp3_path.relative_to(root) _fail(f"{rel} is too small ({size} bytes); expected > {MIN_MP3_BYTES} bytes") mp3_stats.append((mp3_path.relative_to(root), size)) _validate_manifest(manifest_path) return mp3_stats def main(argv: list[str] | None = None) -> int: args = argv if argv is not None else sys.argv[1:] if len(args) != 1: print("Usage: python scripts/validate_bundle.py ", file=sys.stderr) return 1 root = Path(args[0]) mp3_stats = validate_bundle(root) print("OK: media-assets bundle validation passed") print(f"mp3 count: {len(mp3_stats)}") for rel_path, size in mp3_stats: print(f" - {rel_path.as_posix()}: {size} bytes") return 0 if __name__ == "__main__": raise SystemExit(main())