#!/usr/bin/env python3 """Resync each vault hack note's `library_path` to the actual on-disk ROM file. The [Hack] filename standardization renamed ROMs on disk but left the vault notes' `library_path` frontmatter pointing at old names/paths. This matches each note to its real file (by normalized title, falling back to the old path's basename) and rewrites `library_path` + any exact body copy of the old value. Read-only by default; pass --apply to write. ROM listings are read from /tmp/romsync/.txt (one filename per line), pulled from valhalla. """ import os, re, sys, glob VAULT = r"C:\Users\MattC\Documents\Obsidian Vault" HACKS = os.path.join(VAULT, "Pokémon ROM Hacks", "Hacks") LISTS = os.path.join(os.path.dirname(__file__), "..", ".scrape", "romsync") PLAT = {"GBA": "gba", "GBC": "gbc", "GB": "gb", "NDS": "nds", "3DS": "3ds", "PC": "windows"} APPLY = "--apply" in sys.argv def norm(s): s = os.path.splitext(s)[0].lower() s = re.sub(r"\[[^\]]*\]", "", s) # drop [Hack], [2.0.5], [Full], ... s = re.sub(r"\([^)]*\)", "", s) # drop (Gold inc), (Spanish), ... s = re.sub(r"pok[eé]?mon", "", s) # drop the franchise prefix/word s = re.sub(r"[^a-z0-9]", "", s) # keep bare digits (Dark Rising 2) return s # ---- build per-platform file index: normkey -> [filenames] ---- idx = {} for path in glob.glob(os.path.join(LISTS, "*.txt")): plat = os.path.splitext(os.path.basename(path))[0] idx[plat] = {} for line in open(path, encoding="utf-8"): fn = line.strip() if not fn or not re.search(r"\.(gba|gbc|gb|nds|3ds|cci|cxi|zip)$", fn, re.I): continue idx[plat].setdefault(norm(fn), []).append(fn) def fm_get(text, key): m = re.search(rf"(?m)^{key}:\s*(.*)$", text) return m.group(1).strip().strip('"').strip("'") if m else None FUZZY = "--fuzzy" in sys.argv # also write the proposed fuzzy/cross-platform matches def find_exact_all(key): """Exact normalized-key match across ALL platforms (catches wrong platform field).""" hits = [] for p, files in idx.items(): for fn in files.get(key, []): hits.append((p, fn)) return hits # Notes whose only fuzzy candidate is a different/ambiguous hack — held for manual # review, never auto-applied even with --fuzzy. FUZZY_HOLD = { "Bloody Fusion.md", "Too Many Types.md", "HeartGold Generation.md", "FireRed Reignited LeafGreen Regrown.md", "Kanto Complete.md", "Mystery Dungeon - Wigglytuff's Bizarre Adventure.md", } def find_fuzzy(nk): """One key is a substring of the other; require a [Hack] file; must be unique. Skip very short keys (e.g. a hack literally named 'Z') to avoid spurious hits.""" if len(nk) < 4: return [] hits = [] for p, files in idx.items(): for k, fns in files.items(): if len(k) < 4 or not nk: continue if nk in k or k in nk: for fn in fns: if "[hack]" in fn.lower(): hits.append((p, fn)) return sorted(set(hits)) changed = unchanged = unmatched = ambiguous = 0 report = {"CHANGED": [], "UNMATCHED": [], "AMBIGUOUS": [], "FUZZY": []} for md in sorted(glob.glob(os.path.join(HACKS, "*.md"))): text = open(md, encoding="utf-8").read() title = fm_get(text, "title") or os.path.splitext(os.path.basename(md))[0] platform = (fm_get(text, "platform") or "").upper() cur = fm_get(text, "library_path") or "" plat = PLAT.get(platform) note = os.path.basename(md) if not plat or plat not in idx: continue def write_path(newpath): if not APPLY: return new = re.sub(r"(?m)^(library_path:\s*).*$", lambda m: m.group(1) + newpath, text, count=1) # Update body copies ONLY when the old value was an emulation-rom path; # never touch /storage1/labdata/... served-files links. if cur and cur.startswith(("roms/", "/storage1/Emulation/")): new = new.replace(cur, newpath) open(md, "w", encoding="utf-8", newline="").write(new) nk = norm(title) # 1) exact match within the declared platform (auto-apply) cand = idx[plat].get(nk) or (idx[plat].get(norm(os.path.basename(cur))) if cur else None) if cand and len(cand) == 1: newpath = f"roms/{plat}/{cand[0]}" if cur == newpath: unchanged += 1; continue changed += 1; report["CHANGED"].append((note, cur, newpath)); write_path(newpath); continue if cand and len(cand) > 1: ambiguous += 1; report["AMBIGUOUS"].append((note, cand)); continue # 2) cross-platform exact, then fuzzy substring -> PROPOSAL (apply only with --fuzzy) prop = find_exact_all(nk) or find_fuzzy(nk) if len(prop) == 1: p2, fn = prop[0]; newpath = f"roms/{p2}/{fn}" if cur == newpath: unchanged += 1; continue report["FUZZY"].append((note, cur, newpath)) if FUZZY and note not in FUZZY_HOLD: changed += 1; write_path(newpath) continue unmatched += 1; report["UNMATCHED"].append((note, platform, cur)) summary = (f"{'APPLIED' if APPLY else 'DRY-RUN'}{' +FUZZY' if FUZZY else ''}: {changed} changed, " f"{unchanged} already correct, {len(report['FUZZY'])} fuzzy proposals, " f"{unmatched} unmatched (no on-disk file — likely Wanted/gated), {ambiguous} ambiguous") lines = [summary, ""] for tag in ("AMBIGUOUS", "FUZZY", "CHANGED", "UNMATCHED"): rows = report[tag] if not rows: continue lines.append(f"--- {tag} ({len(rows)}) ---") for r in rows: if tag in ("CHANGED", "FUZZY"): lines.append(f" {r[0]}\n {r[1] or '—'} -> {r[2]}") elif tag == "AMBIGUOUS": lines.append(f" {r[0]} -> {r[1]}") else: lines.append(f" {r[0]} [{r[1]}] (cur: {r[2] or '—'})") lines.append("") rep = os.path.join(os.path.dirname(__file__), "..", ".scrape", "resync-report.txt") open(rep, "w", encoding="utf-8").write("\n".join(lines)) print(summary) print("full report ->", os.path.normpath(rep))