diff --git a/Caddyfile b/Caddyfile index 2d8c0ff..f163d6a 100644 --- a/Caddyfile +++ b/Caddyfile @@ -128,6 +128,16 @@ roms.ginnoir.com { reverse_proxy romm:8080 } +# Pokémon ROM-hack library files (box art, guides, spreadsheets) for the +# Obsidian catalog notes. Internal-only — these downloads are private. +# Root is the read-only /storage1/labdata/romhacks/library bind from the proxy +# stack; notes embed art via https://romhacks-files.ginnoir.com//. +romhacks-files.ginnoir.com { + import internal_only + root * /srv/romhacks + file_server browse +} + # ============================================================= # NOTIFICATIONS & RSS — public # ============================================================= diff --git a/bookmarks-domains.html b/bookmarks-domains.html index 8f67d9c..6742c93 100644 --- a/bookmarks-domains.html +++ b/bookmarks-domains.html @@ -49,11 +49,8 @@
nzbget
whisparr
stash -

- -

GAMES / ROMS (internal)

-

roms +
romhacks-files

NOTIFICATIONS & RSS (public)

@@ -101,5 +98,10 @@
plane

+

NOTES / PKM — public (obsidian-livesync syncs from any device) (public)

+

+

obsidian +

+

diff --git a/bookmarks-ports.html b/bookmarks-ports.html index 2049705..4488a28 100644 --- a/bookmarks-ports.html +++ b/bookmarks-ports.html @@ -57,6 +57,11 @@

homarr

+

Notes

+

+

couchdb +

+

Notify

ntfy :8000 @@ -88,6 +93,11 @@
resume

+

Romhacks

+

+

jdownloader :8998 +

+

Roms

romm :8997 diff --git a/stacks/proxy/docker-compose.yml b/stacks/proxy/docker-compose.yml index 2a3ce5b..865c0a4 100644 --- a/stacks/proxy/docker-compose.yml +++ b/stacks/proxy/docker-compose.yml @@ -32,6 +32,10 @@ services: - /config/caddy/data:/data - /config/caddy/config:/config - /storage1/Books:/srv/Books + # Pokémon ROM-hack library (box art + guides + spreadsheets), served + # read-only and LAN-only by the romhacks-files.ginnoir.com site so the + # Obsidian catalog notes can embed art and link guides. + - /storage1/labdata/romhacks/library:/srv/romhacks:ro networks: edge: diff --git a/stacks/romhacks/orchestrator/handoff.py b/stacks/romhacks/orchestrator/handoff.py index 084f48f..a48ece7 100644 --- a/stacks/romhacks/orchestrator/handoff.py +++ b/stacks/romhacks/orchestrator/handoff.py @@ -24,6 +24,7 @@ import re import shutil import subprocess import sys +from urllib.parse import quote INCOMING = pathlib.Path(os.environ.get("INCOMING_DIR", "/incoming")) LIBRARY = pathlib.Path(os.environ.get("LIBRARY_DIR", "/library")) @@ -41,6 +42,51 @@ PATCH_EXTS = {".ips", ".bps", ".ups", ".xdelta"} INPROGRESS = (".part", ".jdownload", ".jdownload.cfg", ".tmp", ".crdownload") FANGAME_HINTS = ("game.exe", "game.ini", ".rxdata", ".rvdata", ".rvdata2", ".rgssad", ".rgss2a", ".rgss3a") ART_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".gif") +# Internal-only Caddy file_server over the library (romhacks-files.ginnoir.com → +# /srv/romhacks). Lets the Obsidian notes embed art / link guides off /storage1. +FILE_BASE = os.environ.get("FILE_BASE", "https://romhacks-files.ginnoir.com") +# Document/spreadsheet extensions that make a genuine guide (a subset of +# GUIDE_EXTS — bare game-data .txt under PBS/Graphics/Plugins is not a guide). +DOC_EXTS = {".html", ".htm", ".pdf", ".xlsx", ".xls", ".ods", ".csv", + ".doc", ".docx", ".rtf"} +GAME_DATA_DIRS = {"pbs", "graphics", "plugins", "audio", "data", "fonts", + "scripts", "optional", "se", "bgm", "tilesets", "sound", + "movies", "backup"} +MAX_GUIDES = 30 + + +def file_url(slug_name, rel): + """URL into the served library, encoding each path segment.""" + parts = [quote(slug_name)] + [quote(seg) for seg in str(rel).split("/")] + return FILE_BASE + "/" + "/".join(parts) + + +def pick_guide_rels(guide_rels, artifact_name): + """Filter raw guide rel-paths to linkable docs/spreadsheets/extras.""" + art_l = (artifact_name or "").lower() + scored = [] + for rel in guide_rels: + low = rel.lower() + parts = low.split("/") + bn = parts[-1] + if bn in ("note.md", "handoff.json"): + continue + ext = os.path.splitext(bn)[1] + in_gamedata = any(seg in GAME_DATA_DIRS for seg in parts[:-1]) + if ext in DOC_EXTS: + if in_gamedata: + continue + scored.append((0, rel)) + elif ext in (".txt", ".md"): + if in_gamedata or len(parts) > 2: + continue + scored.append((1, rel)) + elif ext in ARCHIVE_EXTS: + if bn == art_l: + continue + scored.append((2, rel)) + scored.sort(key=lambda t: (t[0], t[1].lower())) + return [rel for _, rel in scored] # Document / extra files worth keeping (guides, spreadsheets, changelogs, and # supplementary or alternate-version archives). Deliberately excludes raw game # assets (png/audio/scripts) so an extracted tree isn't counted as "guides". @@ -127,7 +173,7 @@ def collect_guides(folder, primary): return guides -def render_note(meta, kind, platform, primary, guides, art, folder): +def render_note(meta, kind, platform, primary, guides, art, folder, slug_name): name = meta.get("name", folder.name) lines = [ "---", @@ -140,8 +186,10 @@ def render_note(meta, kind, platform, primary, guides, art, folder): f"# {name}", "", ] + # Box art — art is copied into the library folder root, so / + # resolves on the file-server. for a in art: - lines.append(f"![[{a.name}]]") + lines.append(f"![|320]({file_url(slug_name, a.name)})") if art: lines.append("") desc = (meta.get("description") or "").strip() @@ -152,12 +200,19 @@ def render_note(meta, kind, platform, primary, guides, art, folder): lines += ["## Download links", ""] lines += [f"- {l}" for l in links] lines.append("") + guide_rels = [str(g.relative_to(folder)) for g in guides] + picked = pick_guide_rels(guide_rels, primary.name if primary else None) + if picked: + lines += ["## Guides & extras", ""] + for rel in picked[:MAX_GUIDES]: + lines.append(f"- [{rel}]({file_url(slug_name, rel)})") + if len(picked) > MAX_GUIDES: + lines.append(f"- …and {len(picked) - MAX_GUIDES} more " + f"(browse the [library folder]({file_url(slug_name, '')}))") + lines.append("") lines += ["## Files", "", - f"- **Artifact:** `{primary.name if primary else 'n/a'}` ({kind}/{platform})"] - if guides: - lines.append(f"- **Guides / extras ({len(guides)}):**") - for g in sorted(guides): - lines.append(f" - `{g.relative_to(folder)}`") + f"- **Artifact:** `{primary.name if primary else 'n/a'}` ({kind}/{platform})", + f"- **Library:** [browse files]({file_url(slug_name, '')})"] lines.append("") return "\n".join(lines) @@ -197,7 +252,7 @@ def process(folder, handled): kind, platform, primary = classify(folder) guides = collect_guides(folder, primary) - note = render_note(meta, kind, platform, primary, guides, art, folder) + note = render_note(meta, kind, platform, primary, guides, art, folder, slug_name) print(f"[handoff] {slug_name}: kind={kind} platform={platform} " f"artifact={primary.name if primary else 'NONE'} guides={len(guides)} art={len(art)}") diff --git a/stacks/romhacks/orchestrator/render_catalog_notes.py b/stacks/romhacks/orchestrator/render_catalog_notes.py index 7094feb..a3b7b19 100644 --- a/stacks/romhacks/orchestrator/render_catalog_notes.py +++ b/stacks/romhacks/orchestrator/render_catalog_notes.py @@ -1,10 +1,74 @@ -import json, re, pathlib, datetime +import json, os, re, pathlib, datetime +from urllib.parse import quote rows = json.load(open("/tmp/catalog_new.json", encoding="utf-8")) META = pathlib.Path("/storage1/labdata/romhacks/metadata") +LIBRARY = pathlib.Path("/storage1/labdata/romhacks/library") OUT = pathlib.Path("/tmp/catalog_notes"); OUT.mkdir(exist_ok=True) TODAY = "2026-06-08" +# Internal-only Caddy file_server over /storage1/labdata/romhacks/library +# (romhacks-files.ginnoir.com → /srv/romhacks, import internal_only). Lets the +# notes embed box art and link guides that Obsidian can't load off /storage1. +FILE_BASE = "https://romhacks-files.ginnoir.com" + +# Genuine guide/spreadsheet documents worth linking. Bare game-data text files +# (PBS/, Graphics/, Plugins/ trees of an RPG-Maker fangame) are NOT guides. +DOC_EXTS = {".html", ".htm", ".pdf", ".xlsx", ".xls", ".ods", ".csv", + ".doc", ".docx", ".rtf"} +ARCHIVE_EXTS = {".zip", ".rar", ".7z"} +GAME_DATA_DIRS = {"pbs", "graphics", "plugins", "audio", "data", "fonts", + "scripts", "optional", "se", "bgm", "tilesets", "sound", + "movies", "backup"} +MAX_GUIDES = 30 + + +def file_url(slug, rel): + """URL into the served library, encoding each path segment (spaces, etc.).""" + parts = [quote(slug)] + [quote(seg) for seg in rel.split("/")] + return FILE_BASE + "/" + "/".join(parts) + + +def load_handoff(slug): + hj = LIBRARY / slug / "handoff.json" + if hj.exists(): + try: + return json.loads(hj.read_text(encoding="utf-8")) + except Exception: + pass + return {} + + +def pick_guides(guides, artifact): + """Filter the raw handoff guide list to linkable docs/spreadsheets/extras. + + Returns rel paths, docs first, capped at MAX_GUIDES (caller notes overflow). + """ + art_l = (artifact or "").lower() + scored = [] + for rel in guides: + low = rel.lower() + parts = low.split("/") + bn = parts[-1] + if bn in ("note.md", "handoff.json"): + continue + ext = os.path.splitext(bn)[1] + in_gamedata = any(seg in GAME_DATA_DIRS for seg in parts[:-1]) + if ext in DOC_EXTS: + if in_gamedata: + continue # buried html/csv inside a game tree = noise + scored.append((0, rel)) + elif ext in (".txt", ".md"): + if in_gamedata or len(parts) > 2: + continue # only top-level readme/changelog/credits + scored.append((1, rel)) + elif ext in ARCHIVE_EXTS: + if rel == artifact or bn == art_l: + continue # that's the rom/game itself, linked elsewhere + scored.append((2, rel)) + scored.sort(key=lambda t: (t[0], t[1].lower())) + return [rel for _, rel in scored] + 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", @@ -85,6 +149,17 @@ def render(r): "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("") + # Box art — embed from the internal Caddy file-server. Fall back to art + # filenames in the metadata folder if the handoff record is missing them. + ho = load_handoff(r["slug"]) + art = ho.get("art") or [] + if not art and (META / r["slug"]).exists(): + art = sorted(p.name for p in (META / r["slug"]).glob("*") + if p.suffix.lower() in (".png", ".jpg", ".jpeg", ".webp", ".gif")) + for a in art: + fm.append(f"![|320]({file_url(r['slug'], a)})") + if art: + fm.append("") cd = clean_desc(desc) if cd: fm += ["## Summary", "", cd, ""] @@ -95,10 +170,20 @@ def render(r): except Exception: pass if links: fm += ["## Download", ""] + [f"- {l}" for l in links] + [""] + # Guides / spreadsheets / extras — link each to the served file. + guides = pick_guides(ho.get("guides", []), r["artifact"]) + if guides: + fm += ["## Guides & extras", ""] + for rel in guides[:MAX_GUIDES]: + fm.append(f"- [{rel}]({file_url(r['slug'], rel)})") + if len(guides) > MAX_GUIDES: + fm.append(f"- …and {len(guides) - MAX_GUIDES} more " + f"(browse the [library folder]({file_url(r['slug'], '')}))") + fm.append("") 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 "")] + f"- **Library:** [browse files]({file_url(r['slug'], '')}) — " + f"`/storage1/labdata/romhacks/library/{r['slug']}/`"] fm.append("") return safe_filename(name), "\n".join(fm)