chore(romhacks): catalog publisher tooling (analyze + render Obsidian notes)

This commit is contained in:
ginnoir
2026-06-08 04:25:58 -05:00
parent ae57c7f661
commit 8224a8fddb
2 changed files with 248 additions and 0 deletions
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Analysis pass for the Obsidian catalog publisher (read-only).
For every routed library/<slug>/ game, derive its catalog fields:
- name, platform (UPPERCASE), kind, base, version, status, generation,
library_path, source link, #guides, #art.
Generation comes from which forum the thread lived in (exports/<channelId>/);
base/version/status are parsed from the staged description.
Emits one JSON object per line (so the caller can dedupe against the existing
vault notes) plus a summary. Writes nothing outside stdout.
"""
import json
import os
import pathlib
import re
LIBRARY = pathlib.Path(os.environ.get("LIBRARY_DIR", "/library"))
METADATA = pathlib.Path(os.environ.get("METADATA_DIR", "/metadata"))
EXPORTS = pathlib.Path(os.environ.get("EXPORTS_DIR", "/exports"))
CHANNEL_GEN = {
"1474174360926290105": "Gen I", "1474174532196761802": "Gen II",
"1474174608587489416": "Gen III", "1474174672210755714": "Gen IV",
"1474174736438132756": "Gen V", "1474174789680632084": "Gen VI",
"1474174859671240898": "Gen VII", "1474174950138183880": "Gen VIII",
"1474175004248903935": "Gen IX", "1474175244209098986": "Joiplay/RPGXP",
"1475605095805747351": "Unique",
}
PLAT_UP = {"gba": "GBA", "gbc": "GBC", "gb": "GB", "nds": "NDS",
"3ds": "3DS", "switch": "Switch", "pc": "PC", "patch": "Patch",
"unknown": ""}
def norm(name):
n = name.lower()
n = re.sub(r"pok[eé]mon", "", n)
n = re.sub(r"[^a-z0-9]+", "", n)
return n
def gen_map():
m = {}
for chan_dir in (EXPORTS.iterdir() if EXPORTS.exists() else []):
if not chan_dir.is_dir():
continue
gen = CHANNEL_GEN.get(chan_dir.name)
if not gen:
continue
for jf in chan_dir.glob("*.json"):
try:
d = json.loads(jf.read_text(encoding="utf-8"))
nm = d.get("channel", {}).get("name")
if nm:
m[norm(nm)] = gen
except Exception:
pass
return m
def parse_desc(desc):
base = ver = status = ""
m = re.search(r"based on\s*:?\s*([A-Za-z0-9 .&'+-]{2,40})", desc, re.I)
if m:
base = m.group(1).strip().rstrip(".").strip()
m = re.search(r"version\s*:?\s*([^\n]{1,30})", desc, re.I)
if m:
ver = m.group(1).strip()
low = desc.lower()
if "completed" in low or re.search(r"\bcomplete\b", low):
status = "Complete"
elif "beta" in low:
status = "Beta"
elif "ongoing" in low or "in development" in low:
status = "Ongoing"
return base, ver, status
def main():
gens = gen_map()
rows = []
for folder in sorted(p for p in LIBRARY.iterdir() if p.is_dir()):
hj = folder / "handoff.json"
if not hj.exists():
continue
h = json.loads(hj.read_text(encoding="utf-8"))
name = h.get("name", folder.name)
slug = h.get("slug", folder.name)
plat = PLAT_UP.get(h.get("platform", "unknown"), "")
desc = ""
mp = METADATA / slug / "metadata.json"
if mp.exists():
try:
desc = json.loads(mp.read_text(encoding="utf-8")).get("description", "")
except Exception:
pass
base, ver, status = parse_desc(desc)
gen = gens.get(norm(name), "")
art = h.get("art", [])
guides = h.get("guides", [])
links = h.get("links", [])
artifact = h.get("artifact")
kind = h.get("kind")
lib_path = (f"/storage1/Emulation/roms/{h.get('platform')}/{artifact}"
if kind == "console" and artifact else
f"/storage1/labdata/romhacks/library/{slug}/")
rows.append({
"name": name, "slug": slug, "norm": norm(name), "platform": plat,
"kind": kind, "base": base, "version": ver, "status": status,
"generation": gen, "library_path": lib_path,
"source": links[0] if links else None, "n_links": len(links),
"n_guides": len(guides), "n_art": len(art), "artifact": artifact,
})
for r in rows:
print(json.dumps(r, ensure_ascii=False))
# summary to stderr-ish (still stdout, prefixed)
from collections import Counter
pc = Counter(r["platform"] for r in rows)
kc = Counter(r["kind"] for r in rows)
print("@@@SUMMARY", json.dumps({
"total": len(rows), "by_platform": dict(pc), "by_kind": dict(kc),
"with_base": sum(1 for r in rows if r["base"] != ""),
"with_gen": sum(1 for r in rows if r["generation"] != ""),
}, ensure_ascii=False))
if __name__ == "__main__":
main()
@@ -0,0 +1,118 @@
import json, re, pathlib, datetime
rows = json.load(open("/tmp/catalog_new.json", encoding="utf-8"))
META = pathlib.Path("/storage1/labdata/romhacks/metadata")
OUT = pathlib.Path("/tmp/catalog_notes"); OUT.mkdir(exist_ok=True)
TODAY = "2026-06-08"
BASES = ["HeartGold","SoulSilver","FireRed","LeafGreen","Omega Ruby","Alpha Sapphire",
"Black 2","White 2","Emerald","Crystal","Platinum","Diamond","Pearl",
"Ruby","Sapphire","Gold","Silver","Black","White","Red","Blue","Yellow",
"Sun","Moon","Sword","Shield","Scarlet","Violet"]
PLAT_TAG = {"GBA":"gba","GBC":"gbc","GB":"gb","NDS":"nds","3DS":"3ds","Switch":"switch","PC":"pc","Patch":"patch","":"unknown"}
def q(v):
return '"' + str(v).replace("\\", "\\\\").replace('"', '\\"') + '"'
def clean_version(v):
v = re.sub(r"^[\s*:_-]+", "", str(v or "")).strip()
return v or ""
def strip_prefix(name):
return re.sub(r"^\s*pok[eé]mon\s+", "", name, flags=re.I).strip()
def safe_filename(name):
return re.sub(r'[\\/:*?"<>|]+', " ", strip_prefix(name)).strip() or "Unknown"
def detect_base(desc, parsed):
# Only trust an explicit "based on : <base>" segment — scanning the whole
# description gives false positives (e.g. "Black Market", "Red Gyarados").
m = re.search(r"based on\s*:?\s*(.{0,40})", desc, re.I)
if not m:
return None
seg = m.group(1)
for spaced, canon in [(r"fire\s*red","firered"),(r"leaf\s*green","leafgreen"),
(r"heart\s*gold","heartgold"),(r"soul\s*silver","soulsilver"),
(r"omega\s*ruby","omegaruby"),(r"alpha\s*sapphire","alphasapphire")]:
seg = re.sub(spaced, canon, seg, flags=re.I)
for b in BASES:
if re.search(r"\b" + re.escape(b) + r"\b", seg, re.I):
return b
return None
def clean_desc(desc):
# drop the markdown download links already captured separately; keep prose
lines = []
for ln in desc.splitlines():
if re.search(r"\bdownload\b", ln, re.I) and "http" in ln.lower():
continue
lines.append(ln)
return "\n".join(lines).strip()
def render(r):
name = r["name"]; title = "Pokémon " + strip_prefix(name)
desc = ""
mp = META / r["slug"] / "metadata.json"
if mp.exists():
try: desc = json.loads(mp.read_text(encoding="utf-8")).get("description","")
except Exception: pass
base = detect_base(desc, r["base"]) or ""
ptag = PLAT_TAG.get(r["platform"],"unknown")
tags = ["hack", f"platform/{ptag}", f"status/{r['status'].lower()}" if r["status"]!="" else None,
f"base/{base.lower().replace(' ','')}" if base!="" else None,
f"kind/{r['kind']}", "source/discord"]
tags = [t for t in tags if t]
fm = [
"---",
f"title: {q(title)}",
f"platform: {q(r['platform'])}",
f"base: {q(base)}",
f"version: {q(clean_version(r['version']))}",
f"status: {q(r['status'])}",
"type: []",
f"generation: {q(r['generation'])}",
f"library_path: {q(r['library_path'])}",
f"source: {q(r['source'] or '')}",
f"added: {TODAY}",
"play_status: Unplayed",
f"tags: [{', '.join(tags)}]",
"---",
"",
f"# {title}",
"",
]
kindline = {"console":"Console ROM hack — in RomM.","fangame":"RPG-Maker / Joiplay fan-game (PC) — not in RomM.",
"patch":"Patch file — apply to a base ROM.","unknown":"Imported."}.get(r["kind"],"Imported.")
fm.append(f"> [!info] Auto-imported from the Discord catalog ({r['generation']}) on {TODAY}. {kindline}")
fm.append("")
cd = clean_desc(desc)
if cd:
fm += ["## Summary", "", cd, ""]
# download
links = []
if mp.exists():
try: links = json.loads(mp.read_text(encoding="utf-8")).get("links",[])
except Exception: pass
if links:
fm += ["## Download", ""] + [f"- {l}" for l in links] + [""]
fm += ["## Files", "",
f"- **Artifact:** `{r['artifact'] or 'n/a'}`",
f"- **Library:** `/storage1/labdata/romhacks/library/{r['slug']}/`"
+ (f"{r['n_guides']} guide/extra file(s)" if r['n_guides'] else "")]
fm.append("")
return safe_filename(name), "\n".join(fm)
made = []
for r in rows:
fn, body = render(r)
(OUT / (fn + ".md")).write_text(body, encoding="utf-8")
made.append((fn, r))
print("rendered", len(made), "notes to /tmp/catalog_notes/")
# show 2 samples: a console GBA hack and a fangame
for want in ("console","fangame"):
for fn, r in made:
if r["kind"] == want:
print("\n========== SAMPLE (%s): %s.md ==========" % (want, fn))
print((OUT / (fn + ".md")).read_text(encoding="utf-8")[:1400])
break