Catalog cleanup pass across gb/gbc/gba/nds/3ds: - Standardize nonstandard hack filenames to `Pokemon - <Name> [Hack].<ext>`; remove verified byte-identical duplicates (conform-rom-names.sh). - Resolve 9 version-conflicts via side-by-side emulator comparison (resolve-version-conflicts.sh): promote newer builds, keep genuine distinct hacks, fix a mislabeled Dark Rising slot (was the Digimon crossover "Worlds Collide"), and rebuild Mega Power 5.77 + HeartGold Generations v2.0 fresh from their patches. - Resync 286 vault notes' `library_path` to the real on-disk [Hack] files and fix 15 stale platform fields (resync-vault-library-paths.py). - Regenerate MOC tables + wiki/catalog.json (396 notes). Also includes pending Playnite PC-hack setup/repair scripts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
5.9 KiB
Python
132 lines
5.9 KiB
Python
#!/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/<plat>.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))
|