#!/usr/bin/env python3 """ Standardize valhalla Hack-library filenames to: Pokemon - [Hack][][]. Rule-based + self-fetching: queries valhalla for every file still tagged "(Hack)" anywhere under the roms tree and rewrites it. - mechanical: " (Hack)" -> " [Hack]" (preserves other (...) like (FRLG+)) - OVERRIDES: platform-port fan-games whose stem carries "--" get the version pulled into [ver] and the OS kept as a tag. - SKIP: non-game patcher utilities are left untouched. Dry-run prints OLD -> NEW; --apply runs `mv -n` over SSH. Idempotent: already-[Hack] files don't match the (Hack) query, so re-runs are no-ops. """ import subprocess, sys, shlex, os APPLY = "--apply" in sys.argv HOST = "valhalla" ROOT = "/storage1/Emulation/roms" # Non-game utilities — leave alone. SKIP = { "Pokemon - DeltaPatcherLite (Hack) [Installer].zip", "Pokemon - Patcher (Hack) [Installer].zip", } # Stem-encoded version/OS ports -> canonical [Hack][ver][OS]. OVERRIDES = { "Pokemon - Reborn-19.5.0-linux (Hack) [Linux].zip": "Pokemon - Reborn [Hack][19.5.0][Linux].zip", "Pokemon - Rejuvenation-13.5.0-linux (Hack) [Linux].zip": "Pokemon - Rejuvenation [Hack][13.5.0][Linux].zip", "Pokemon - Reborn-19.5.0-macos (Hack) [macOS].zip": "Pokemon - Reborn [Hack][19.5.0][macOS].zip", "Pokemon - Rejuvenation-13.5.0-macos (Hack) [macOS].zip": "Pokemon - Rejuvenation [Hack][13.5.0][macOS].zip", } def new_name(base): if base in SKIP: return None if base in OVERRIDES: return OVERRIDES[base] if " (Hack)" in base: return base.replace(" (Hack)", " [Hack]") return None def fetch(): cmd = f"find {shlex.quote(ROOT)} -type f -name '*(Hack)*'" out = subprocess.run(["ssh", HOST, cmd], capture_output=True, text=True, check=True).stdout return sorted(p for p in out.splitlines() if p.strip()) def main(): paths = fetch() cmds, special, mech, skipped = [], [], 0, [] for full in paths: d, base = os.path.dirname(full), os.path.basename(full) nn = new_name(base) if nn is None: skipped.append(base) continue cmds.append(f"mv -n -- {shlex.quote(full)} {shlex.quote(d + '/' + nn)}") if base in OVERRIDES: special.append((base, nn)) else: mech += 1 print(f"Found {len(paths)} files still tagged (Hack).\n") print(f"Mechanical (Hack)->[Hack] swaps : {mech}") print(f"Special version/OS overrides : {len(special)}") for o, n in special: print(f" {o}\n -> {n}") print(f"Skipped (non-game utilities) : {len(skipped)}") for s in skipped: print(f" {s}") if not APPLY: print("\n(dry-run — pass --apply to execute over SSH)") return # NB: pipe bytes with explicit \n line endings. On Windows, text=True wraps # stdin in a TextIOWrapper that rewrites \n -> \r\n, which bash would append # as a stray CR onto every destination filename. Encoding ourselves avoids it. script = "set -e\n" + "\n".join(cmds) + "\n" r = subprocess.run(["ssh", HOST, "bash -s"], input=script.encode("utf-8")) print(f"\napplied {len(cmds)} renames (exit {r.returncode})") sys.exit(r.returncode) if __name__ == "__main__": main()