#!/usr/bin/env python3 """ migrate_replay_ids.py One-shot migration: renames decimal replay directories to hex and updates the _id field inside each replay_data.json.gz to match. Run on the server: python3 migrate_replay_ids.py python3 migrate_replay_ids.py --dry-run # preview without touching anything Reads STORAGE_VOL_PATH from environment or TSSBOT/.env. """ from __future__ import annotations import argparse import gzip import json import os import sys from pathlib import Path _HERE = Path(__file__).resolve().parent # Try loading .env if python-dotenv is available try: from dotenv import load_dotenv load_dotenv(dotenv_path=_HERE / ".env") except ImportError: pass STORAGE_VOL_PATH = os.environ.get("STORAGE_VOL_PATH", "").strip() if not STORAGE_VOL_PATH: print("ERROR: STORAGE_VOL_PATH not set", file=sys.stderr) sys.exit(1) REPLAYS_DIR = Path(STORAGE_VOL_PATH) / "REPLAYS" / "TSS" def is_decimal_id(name: str) -> bool: """True if the directory name looks like a plain decimal integer (not hex).""" try: int(name) return True except ValueError: return False def to_hex(decimal_str: str) -> str: return hex(int(decimal_str))[2:].lower() def migrate_one(src_dir: Path, *, dry_run: bool) -> tuple[str, str] | None: """ Rename src_dir from decimal to hex, update _id inside replay_data.json.gz. Returns (old_name, new_name) on success, None if already hex or skipped. """ name = src_dir.name if not is_decimal_id(name): return None hex_name = to_hex(name) dst_dir = src_dir.parent / hex_name if dst_dir.exists(): print(f" SKIP {name} → {hex_name} (destination already exists)") return None gz_path = src_dir / "replay_data.json.gz" if gz_path.exists(): try: with gzip.open(gz_path, "rb") as fh: data = json.loads(fh.read().decode("utf-8")) except Exception as exc: print(f" ERROR reading {gz_path}: {exc}") return None data["_id"] = hex_name if not dry_run: with gzip.open(gz_path, "wb") as fh: fh.write(json.dumps(data, ensure_ascii=False).encode("utf-8")) if not dry_run: src_dir.rename(dst_dir) return name, hex_name def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("--dry-run", action="store_true", help="Print what would happen without changing anything") args = parser.parse_args() if not REPLAYS_DIR.is_dir(): print(f"ERROR: {REPLAYS_DIR} does not exist", file=sys.stderr) sys.exit(1) entries = sorted(p for p in REPLAYS_DIR.iterdir() if p.is_dir()) candidates = [p for p in entries if is_decimal_id(p.name)] if not candidates: print("No decimal-named replay directories found. Nothing to do.") return mode = "DRY RUN — " if args.dry_run else "" print(f"{mode}Found {len(candidates)} decimal director(ies) to migrate in {REPLAYS_DIR}\n") ok = skipped = errors = 0 for src_dir in candidates: result = migrate_one(src_dir, dry_run=args.dry_run) if result is None: skipped += 1 else: old, new = result tag = "[dry-run] " if args.dry_run else "" print(f" {tag}{old} → {new}") ok += 1 print(f"\nDone. migrated={ok} skipped={skipped} errors={errors}") if args.dry_run: print("(dry run — nothing was changed)") if __name__ == "__main__": main()