A project of the Move Weight Foundation, an Oklahoma non-profit corporation with 501(c)(3) status pending.
The *arr for games. Request a title, ROMarr finds it, grabs it, and files it into your game library.
It is not tied to one library. RomM, Gaseous and Retrom are all supported, chosen with a single setting:
LIBRARY_KIND=romm # or gaseous, or retrom
LIBRARY_URL=http://your-library:8080
LIBRARY_API_KEY=... # or LIBRARY_USERNAME / LIBRARY_PASSWORD
Existing installs need no changes: LIBRARY_KIND defaults to romm and the
old ROMM_* variables are still read.
Adding a backend means implementing four methods -- reachable, count, games,
rescan -- in romarr/libraries.py. Nothing else in the service learns which
library is attached.
*The arr for games. ROMarr watches for game requests, searches your indexers through Prowlarr, hands the winner to your download client, and files the ROM into RomM where it can actually be played.
If you run Radarr for films and Sonarr for TV, this is the missing one.
GG Requestz ──request──▶ ROMarr ──search──▶ Prowlarr ──▶ indexers
│ │
│◀──────────── releases ───────────┘
│
├──grab──▶ qBittorrent / NZBGet
│
└──import──▶ RomM library ──▶ playable
Radarr and Sonarr solved "request a thing, get the thing, filed correctly." Nobody had done it for ROMs, and ROMs have their own problems that a film downloader does not:
- The wrong file is usually in the box. Game releases ship readmes, box art,
and often several regional dumps in one archive. Importing
readme.nfointo your library helps nobody. - Size is a signal, not a preference. A 40 GB "SNES" result is a romset, a PC port, or a bad match. For a cartridge, smaller is usually more correct — the opposite of video.
- Platform names are ambiguous. "Super Nintendo Entertainment System" contains "Nintendo Entertainment System". Naive matching sends every SNES request to the NES folder. (There is a test for this, because it happened.)
- Searches Prowlarr, filtered to console and PC-game categories at the query rather than after, so films never enter the ranking in the first place
- Scores releases on seeders, region (USA → World → Europe → Japan), and size sanity, and rejects prototypes, hacks and demos
- Grabs via qBittorrent (torrent), SABnzbd or NZBGet (usenet), routing each release to a client that speaks its protocol
- Picks the ROM out of the finished download, preferring the platform's own extensions in order and the best regional dump when several are present
- Imports into RomM at
<library>/<platform-slug>/, refusing to overwrite unless told - Triggers a RomM rescan so the game appears without a manual refresh
- Translates download paths when the client runs elsewhere, the way Radarr and Sonarr's remote path mappings do — without it the import fails with "download path does not exist" while the file sits right there
- Keeps History, Wanted and settings across a restart
- A web UI shaped like the other *arrs: Library, Wanted, Activity, Settings, System
- Zip-slip is blocked. Archive entries that resolve outside the library root are dropped, not sanitised — a release that needs sanitising is not one to trust. Tested.
- Prowlarr API keys never reach a browser or a log. Prowlarr's
downloadUrlis a link back to itself with the key in the query string. That URL is passed to the download client, which fetches it server-side — the same thing Radarr and Sonarr do, and the only way usenet can work at all, since an NZB has no magnet equivalent. It is redacted everywhere it could be displayed or logged. Tested. - Nothing overwrites silently. An existing ROM is left alone and reported.
Cartridge-era systems, deliberately: NES, SNES, Game Boy / Color / Advance, N64, Genesis / Mega Drive, Master System, Game Gear, Atari 2600 / 7800, Lynx, TurboGrafx-16, WonderSwan, Neo Geo Pocket, Virtual Boy.
Disc-based platforms are excluded on purpose. Their images are gigabytes, often multi-file, and cannot be streamed into a browser emulator — which is the point of feeding RomM in the first place.
docker run -d --name romarr -p 7878:7878 \
-e PUID=1000 -e PGID=1000 \
-e PROWLARR_URL=http://prowlarr:9696 -e PROWLARR_API_KEY=... \
-e LIBRARY_URL=http://romm:8080 -e LIBRARY_USERNAME=romarr -e LIBRARY_PASSWORD=... \
-e QBITTORRENT_URL=http://qbittorrent:8080 \
-v ./config:/config -v /path/to/roms:/roms -v /path/to/downloads:/downloads \
ghcr.io/blizzhacker/romarr:latestThere is a docker-compose.yml with every setting
commented, which is the easier way in. Images are published for linux/amd64,
linux/arm64 and linux/arm/v7, so the same tag runs on a NAS, a Pi or a
server.
Three things worth knowing:
PUID/PGIDdecide who owns the ROMs. ROMarr writes files that your library application has to read. Give it the same ids as that application, orid -u/id -gfor your media user. Only/configis chowned — never the library or downloads volumes, because a recursive chown of a multi-terabyte share at every boot is not something an image should do to you.- On the downloads volume, the container side is the one that has to match
your download client. In
-v /mnt/downloads/complete:/downloads/completethe left side is wherever the files really live on the host, and the right side has to be the path the client reports — ROMarr asks the client where a finished download is and then opens that path itself, so the two have to agree. qBittorrent shows it as "Save path" under Options → Downloads, SABnzbd as "Completed Download Folder" under Config → Folders. A client running on the host rather than in a container reports host paths, which makes both sides identical. Get this wrong and the import looks for a finished file where it is not: you get "download path does not exist in this container" while the file sits in plain sight. When the paths genuinely cannot be made to match, set a mapping under Settings → Media Management instead. /configholds the decisions you make in the UI, and a setting saved there outranks the environment from then on. That is deliberate — an environment default must not silently undo a choice — but it means changingLIBRARY_PATHafter first run has no effect. Change it on the Settings page.
One line:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/BlizzHacker/romarr/main/proxmox/ct/romarr.sh)"git clone https://github.com/BlizzHacker/romarr.git && cd romarr
pip install -r requirements.txt
cp .env.example .env # then edit it
set -a; . ./.env; set +a # nothing loads .env for you -- no dotenv dependency
python -m romarr| Variable | What it is |
|---|---|
PROWLARR_URL / PROWLARR_API_KEY |
your Prowlarr, for searching |
LIBRARY_KIND |
romm, gaseous or retrom (default romm) |
LIBRARY_URL / LIBRARY_USERNAME / LIBRARY_PASSWORD |
your game library, for the rescan trigger. ROMM_* still works |
LIBRARY_PATH |
path to the library root — inside the container when using Docker, where the compose files use /roms. Formerly ROMM_LIBRARY, still accepted. Stored on first run; change it on the Settings page afterwards |
QBITTORRENT_URL / QBITTORRENT_USER / QBITTORRENT_PASS |
torrent client |
SABNZBD_URL / SABNZBD_API_KEY |
usenet client, optional |
NZBGET_URL / NZBGET_USER / NZBGET_PASS |
usenet client, optional |
QBITTORRENT_CATEGORY / SABNZBD_CATEGORY / NZBGET_CATEGORY |
what ROMarr labels its own downloads with, per client. Default romarr |
GGREQUESTZ_URL |
shows the request front-end's status, optional |
ROMARR_DATA |
where history and settings are kept |
PUID / PGID / TZ |
Docker image only — see above |
A protocol with no client configured cannot be grabbed at all — results are found, ranked, and then refused. The Download Clients page names any protocol in that state rather than leaving you to work it out.
ROMarr labels its downloads romarr, so its jobs are distinguishable from
everything else in a client you already use for other things — the same reason
Radarr and Sonarr each use their own.
The category does not have to exist in the client first. SABnzbd 5.0.4
ships *, movies, tv, audio and software, and romarr is none of
them; an undefined category is accepted, kept verbatim on the job, and still
matched by the history filter ROMarr uses to notice a finished download.
Defining it is still worth doing if you want the download in a folder of its
own or a post-processing script — ROMarr does not care either way, because it
takes the finished path from SABnzbd's own storage field rather than guessing
where the category put it.
Set SABNZBD_CATEGORY=software if you would rather use a built-in, or give two
ROMarrs sharing one client different categories so they stop claiming each
other's downloads.
A protocol with no client configured cannot be grabbed at all — results are found, ranked, and then refused. The Download Clients page names any protocol in that state rather than leaving you to work it out.
ROMarr drives several library servers at once. Add them under Settings → Libraries: each carries its own address, credentials and filesystem path, and one is marked default.
Routing is by platform. A library with platform rules receives only those platforms; anything unmatched goes to the default — so "N64 goes to Retrom" is one row on that page rather than a second ROMarr. A platform rule beats the default, because naming a platform is the more specific statement. Two servers of the same kind are fine, and are the common case: a RomM for the house and a RomM for a child's device.
Each server needs its own path, as ROMarr sees it. Two libraries may share a path if you genuinely point both applications at one tree, but nothing assumes it. The Libraries page flags a server that answers while its path is missing here, which is almost always a volume that was never mounted into ROMarr.
The environment seeds the first library on first run, so an existing install comes up already configured. Everything after that is managed on the page.
The Library view aggregates every server and badges each poster with its source; one unreachable server is reported as a named row rather than an empty shelf.
Gaseous has no scan trigger — there is no such call in its API. It picks up new
files through two background tasks of its own: TitleIngestor every minute,
which processes Gaseous's Import directory, and LibraryScan every 1440
minutes, which walks the configured library paths. A ROM filed into a Gaseous
library path can therefore take up to a day to appear, and nothing ROMarr sends
changes that. Either point that library's path at Gaseous's Import directory, or
lower LibraryScan's interval in Gaseous.
RomM and Retrom are both told directly and appear at once. For RomM, the account ROMarr uses needs permission to run tasks — without it the rescan is refused with a 403 and the ROM waits for RomM's next scheduled scan.
If your download client runs in a different container or host, it reports paths in its filesystem. Set the mapping under Settings → Media Management, or in the settings file:
"remote_path_mappings": [
{ "remote": "/downloads", "local": "/mnt/downloads" }
]The longest matching prefix wins, so a specific mapping overrides a broader one rather than depending on which was added first. A mapping cannot help if the volume is not mounted here at all — that stays a mount problem. Either way the log names both the path the client reported and what ROMarr made of it, which is what tells the two apart: no mapping matched, or a mapping matched and its local side is wrong.
Use a dedicated RomM account, not your admin one. ROMarr needs only enough to trigger a scan.
python -m pytest tests/ -qThe decisions that matter — which release to take, which file is the ROM, and whether an archive entry is safe to extract — are pure functions with no network, so they are tested directly rather than mocked around.
MIT.