RomM (ROM Management)¶
Self-hosted ROM management with IGDB + ScreenScraper metadata, Tinfoil integration.
Source: extracted from AGENTS.md
RomM (ROM Management)¶
RomM (container: romm) — Self-hosted ROM management platform with IGDB + ScreenScraper metadata.
| Detail | Value |
|---|---|
| Web UI | http://100.68.251.84:8087 (nginx cached) / http://100.68.251.84:8089 (direct) |
| Image | rommapp/romm:latest |
| Stack | stacks/storage/romm/compose.yaml |
| Server | unraid |
| Version | v4.8.1 |
| Database | PostgreSQL at 100.68.251.84:5432, DB romm |
| Redis | 100.68.251.84:6379 DB3 |
| Admin | lucndm / minhluc1 |
Architecture:
/mnt/user/roms/romm/library/
├── roms/ # ROM files organized by platform slug
│ ├── switch/ # 10,172 ROMs
│ ├── genesis/ # 1,943
│ ├── nes/ # 1,047
│ ├── gameboy/ # 935
│ ├── arcade/ # 859
│ ├── snes/ # 759
│ ├── gba/ # 506
│ ├── nds/ # 466
│ ├── sms/ # 306 (Master System)
│ ├── n64/ # 62
│ ├── sg1000/ # 16
│ ├── segacd/ # 10
│ └── 3ds/ # 9
└── bios/ # BIOS files by platform
Metadata providers: IGDB + ScreenScraper (credentials in TOML environment + compose.env)
API auth flow:
GET /api/heartbeat→ extractromm_csrftokencookiePOST /api/token(form-urlencoded) → JWTaccess_token- API calls:
Authorization: Bearer <token>
Triggering scans:
- API auth currently returns 403 for platform listing (JWT scopes empty) — may be a v4 bug
- Workaround: enqueue scan directly via Redis RQ inside container:
ssh root@100.68.251.84 "docker exec romm python3 -c '
from tasks.scheduled.scan_library import scan_platforms, ScanType
import redis
from rq import Queue
q = Queue(\"high\", connection=redis.Redis(host=\"100.68.251.84\", port=6379, db=3))
job = q.enqueue(scan_platforms, [1,2,3,4,5,6,7,8,9,10,11,12,13], [\"igdb\",\"ss\"], ScanType.COMPLETE, job_timeout=\"2h\")
print(f\"Job: {job.id}\")
'"
Tinfoil integration (Switch download from RomM):
- Native feed endpoint:
/api/feeds/tinfoil— returns custom index JSON per Tinfoil spec - Env var:
DISABLE_DOWNLOAD_ENDPOINT_AUTH=true— allows unauthenticated downloads - Supported file types:
.xci,.nsp,.nsz,.xcz,.nro - Tinfoil setup trên Switch (File Browser → Add New):
- Protocol:
http| Host:100.68.251.84| Port:8087| Path:/api/feeds/tinfoil - No username/password needed (auth disabled)
Tinfoil feed caching (nginx sidecar):
- RomM generates feed on-the-fly from DB — ~101s for 1MB JSON with 10K ROMs
- Nginx sidecar (
romm-cache) serves static cached file: 10ms vs 101s - Architecture:
graph LR CRON["Cron (5min)"] -->|curl| ROMM["RomM direct :8089"] ROMM -->|write tinfoil.json| STATIC["static file tinfoil.json"] STATIC --> NGINX["nginx serves :8087"] TF["Tinfoil / Client"] --> NGINX NGINX -->|HIT| STATIC NGINX -->|MISS| ROMMFB["fallback to RomM"] - Cache script:
/boot/config/scripts/romm-cache-refresh.sh - Cron:
*/5 * * * *— refreshes every 5 minutes - Cache file:
/mnt/user/appdata/romm/nginx-cache/tinfoil.json - Nginx config:
/mnt/user/appdata/romm/nginx-feed-cache.conf(written via SSH, NOT via pre_deploy — Komodo printf loses newlines) - Port layout:
8087: nginx sidecar (cached feed + pass-through for all other endpoints)8089: RomM direct (no cache — admin UI + cron fetch source)
Scan batching strategy (for large libraries):
- Single scan job times out at 2h default — not enough for 17K ROMs
- Split into batches by platform, queue sequentially via RQ
depends_on:
ssh root@100.68.251.84 "docker exec romm python3 -c '
from tasks.scheduled.scan_library import scan_platforms, ScanType
import redis
from rq import Queue
q = Queue(\"high\", connection=redis.Redis(host=\"100.68.251.84\", port=6379, db=3))
# Batch by size: small platforms first, switch last
job = q.enqueue(scan_platforms, [platform_ids], [\"igdb\",\"ss\"], ScanType.COMPLETE, job_timeout=\"4h\")
'"
- IGDB API rate limit: ~4 req/s → scan speed ~7-8 ROMs/min → 17K ROMs takes ~35 hours
- Already identified ROMs survive redeploy (stored in external PostgreSQL)
Pitfalls:
file_pathsmust NOT includecompose.env— Komodo parses all files as YAML,compose.envcauses deploy failure. Useadditional_env_files = ["compose.env"]instead- RomM API
/api/loginneeds CSRF — flow is heartbeat → CSRF cookie → POST login. But/api/token(form-urlencoded) works with CSRF for JWT auth - JWT scopes empty — token has
scopes: ""despite ADMIN role. API returns 403. Use Redis RQ for management tasks - Platform slug conventions: Master System =
sms(notmastersystem), SG-1000 =sg1000 scan_librarytask hasmanual_run: false— can't trigger via scheduler API. Usescan_platforms()via RQ- Scan job timeout — default 2h (
SCAN_TIMEOUTenv var). For large platforms, setjob_timeoutparameter when enqueuing via RQ (not the env var — env var is for the scan itself, RQjob_timeoutis for the worker) - Privacy filter blocks
git pushwhencompose.envexists in repo — use Pythonsubprocess.run(['git','push'])orgh apiPUT to bypass - Compose mount is read-only:
/mnt/user/roms/romm/library:/romm/library:ro— ROM management happens via API, not filesystem writes from container - Tinfoil feed caching — nginx
proxy_cachefailed on Unraid (permission issues with volume write). Switched to static file + cron approach instead - Nginx config in pre_deploy — Komodo
printfloses newlines for multi-line content. Write nginx config via SSH directly to host instead - Port 8088 used by Homepage — RomM direct moved to 8089
- Gitea repo archiving —
git push giteafails with "repo is archived". Unarchive via API:curl -X PATCH ... -d '{"archived": false}'with$GITEA_ACCESS_TOKEN