# romhacks stack — automated Pokémon rom-hack ingestion from a Discord server's # per-generation threads into the RomM library. # # FLOW # romhacks (orchestrator) jdownloader (jlesage) RomM (roms stack) # ┌───────────────────────┐ ┌──────────────────┐ ┌──────────────┐ # │ DiscordChatExporter ──▶│ JSON │ FolderWatch picks│ files │ scans │ # │ parse links + art ───▶│ crawljob│ up .crawljob, │ ───────▶ │ /storage1/ │ # │ stage metadata ───▶│ ──────▶ │ resolves ~all │ │ Emulation │ # └───────────────────────┘ │ hosts, downloads │ └──────────────┘ # └──────────────────┘ # # WHY THIS SHAPE # - Member-only access => no official Discord bot is possible. DiscordChatExporter # (DCE) does a READ-ONLY export with a USER token. Self-token use violates # Discord ToS, so this is wired for an ALT account (see stack.env) to isolate # ban risk from the main account. # - The download links span many file hosts (Mega/Drive/MediaFire/Pixeldrain/...), # so JDownloader2 (jlesage image) does the actual fetching — it auto-resolves # hundreds of hosts and handles waits/captcha via its noVNC UI. # - DCE `--media` downloads the inline thread images (box art / screenshots); the # orchestrator stages those + the thread blurb as metadata for a later RomM # custom-cover hand-off (dovetails with the deck-sgdb art pipeline). # # INERT BY DEFAULT. Does nothing until armed: DISCORD_TOKEN/GUILD_ID are empty and # DRY_RUN=true, so even if deployed it idles and never enqueues a download. See # README.md for the arming checklist. # # BAKED IMAGE (not bind) for the orchestrator: Portainer git-stack checkouts do NOT # reliably materialize sibling repo files at relative FILE bind paths (Docker then # auto-creates a directory — see the `share` stack note). So the orchestrator's # scripts + channels.json are baked into an image built from ./orchestrator. # # That image is built by Gitea Actions and pushed to the self-hosted registry — # NOT by a compose `build:` context. Portainer's git auto-update only ever runs # `docker compose pull`, never `build`, so a `build:` + local-only `image:` name # froze this stack: every poll died on "pull access denied for # homelab/romhacks-orchestrator" and the stack never advanced past its last good # commit. Same pattern as famapp. See .gitea/workflows/build-romhacks-orchestrator.yml. # Editing channels.json or the scripts => push => the workflow rebuilds :latest, # and watchtower/the next redeploy picks it up. # # Tiered binds: working state (exports/state/crawljobs + JD config) -> /config # (SSD); bulk output + art -> /storage1/labdata/romhacks (ZFS, root-auto-created). # # Only jdownloader joins `edge` (Caddy proxies its noVNC UI, internal-only). services: romhacks: image: registry.ginnoir.com/ginnoir/romhacks-orchestrator:latest pull_policy: always container_name: romhacks restart: unless-stopped networks: [romhacks] env_file: - stack.env environment: # Hand-off paths. /data is the single parent mount so incoming->library # moves are instant renames (not cross-device copies); /emulation is the # RomM Structure-A roms parent so console hacks land in roms//. - METADATA_DIR=/data/metadata - INCOMING_DIR=/data/incoming - LIBRARY_DIR=/data/library - EMULATION_DIR=/emulation - HANDOFF_STATE=/state/handled.json volumes: - /config/romhacks/exports:/exports - /config/romhacks/state:/state - /config/romhacks/crawljobs:/crawljobs - /storage1/labdata/romhacks:/data - /storage1/Emulation/roms:/emulation jdownloader: image: jlesage/jdownloader-2:latest container_name: jdownloader restart: unless-stopped networks: [romhacks, edge] env_file: - stack.env environment: # root so it can write to the ZFS-tier /output bind (dirs auto-created as root) - USER_ID=0 - GROUP_ID=0 volumes: - /config/romhacks/jd:/config # shared with the orchestrator: it drops .crawljob files here; the JD # FolderWatch extension must be enabled + pointed at /watch (one-time, README). - /config/romhacks/crawljobs:/watch - /storage1/labdata/romhacks/incoming:/output ports: - "8998:5800" networks: romhacks: name: romhacks driver: bridge edge: name: edge external: true