feat(romhacks): serve library over internal Caddy file_server; notes embed art + guide links
Deploy to valhalla / deploy (push) Has been cancelled

- proxy: read-only bind /storage1/labdata/romhacks/library -> /srv/romhacks
- Caddyfile: romhacks-files.ginnoir.com (internal_only) file_server + browse
- render_catalog_notes.py + handoff.py: embed box art and link guides/
  spreadsheets via https://romhacks-files.ginnoir.com/<slug>/<file>
  (handoff art/guides; guide filter drops buried game-data txt, caps at 30)
- regenerate bookmarks
This commit is contained in:
ginnoir
2026-06-08 04:44:38 -05:00
parent 8224a8fddb
commit 9210cd23f8
6 changed files with 181 additions and 15 deletions
+63 -8
View File
@@ -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 <slug>/<name>
# 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)}")