Repository navigation
Releases: BlizzHacker/romarr
Release list
v1.6.0 — real RetroArch (NCI)
ROMarr v1.6.0 — real RetroArch, over the interface RetroArch actually has.
RetroArch Network Control Interface (#65). The "headless RetroArch stream server" ROMarr used to talk to answered GET /api/play/route — an HTTP endpoint no release of RetroArch has ever had. Real RetroArch exposes the Network Control Interface: UDP, port 55355, plain-text commands (network_cmd_enable = "true" in retroarch.cfg). ROMarr now speaks it.
RetroArchNCIduck-types the existing stream-server slot and ridesStreamSourcesbetween the HTTP server and Moonlight in confidence order — the old StreamServer still works for whoever's custom streamer answers it; nothing was deleted.- Read-only, enforced at the wire: the single send path allowlists
VERSION,GET_STATUS,GET_CONFIG_PARAMand raises on anything else —LOAD_COREcan never leave this class, and a test scans every datagram a full probe sends to prove it. - Honest about what NCI cannot say. It has no installed-core list (upstream's
LIST_CORESis post-1.19.1 and deliberately unused), so: a loaded game'ssystem_idproves that platform streams; everything else answers unknown, never "no" — with operator-readable wording, including the UDP ambiguity (silent host = down, firewalled, or NCI off; ICMP refusal gets its own sentence naming the fix).GET_STATUSparsing verified against RetroArch v1.19.1'scommand.cand master's crc32-less shape, including commas inside basenames. - Config:
RETROARCH_NCI_HOST/RETROARCH_NCI_PORT(default 55355), a General-page status row, docs in README/INSTALL/docker-compose. The breaker + TTL discipline carries over: only successes are cached, an optional feature never hangs a page.
63 new tests on an injected fake UDP transport (no sockets). Full suite: 2345 passed, 3 skipped.
v1.5.0 — MiNERVA as a plugin; torrent-only sources grabbable
ROMarr v1.5.0 — MiNERVA as a plugin; torrent-only sources become grabbable.
MiNERVA moves to the ROM Hub plugin layer (#64). The v1.4.0 indexer driver — a class welded into indexers.py — is gone. MiNERVA Archive now lives in its own repo, BlizzHacker/rom-hub-minerva: a sandboxed ROM Hub plugin declaring search + torrent capabilities, network-allowlisted by manifest, installed like the 23 plugins already on a live box. Zero lines of MiNERVA-specific code remain in ROMarr's core.
What ROMarr gains instead is general infrastructure, not source-specific:
- Torrent-only plugin results are now grabbable. The
hub://fetch lane is HTTP-only by design (the Hub is deliberately not a torrent client), so a source that distributes magnets only — MiNERVA is one — could be searched but never grabbed.hub.resolve_torrents()now runs a plugin'storrentcapability over magnet-bearing rows (grouped per plugin, one sandboxed subprocess each), producing a host-gated, tracker-augmented magnet that goes to your torrent client like any indexer magnet. Failures degrade to the bare magnet, never raise out of a search. - Slow sources stop masquerading as broken ones.
hub.searchgives its HTTP fetcher the call's timeout budget (min 60s, default 90s). MiNERVA measures 21–25s per query live; the old 30s httpx default inside a 30s RPC turned "slow" into "plugin error".
Proven live end-to-end through the production sandbox on the release box: sandboxed search returned 11 real results from minerva-archive.org, resolve_torrent produced a magnet that passed the Hub's own check_magnet gate with all 12 declared trackers. The plugin carries 16 tests against the real rom_hub_sdk; ROMarr carries 8 new tests pinning both release lanes and every resolve failure path. Full suite: 2282 passed, 3 skipped.
Kept from 1.4.x (never source-specific): the recursive collections presence scan and size=0-means-unknown scoring.
One thing to know: a MiNERVA torrent is a whole dump group ("TOSEC – Nintendo"), not one game — that's the archive's distribution model, and the result carries the exact file path so you can see what you're grabbing.
v1.4.2 — auto-update no longer starved by a dead download
ROMarr v1.4.2 — the auto-update can no longer be starved by a dead download.
Found live rolling out v1.4.1: UpdateCheck deferred for hours behind nine queue rows that had been dead for days. Mislabeled public-tracker grabs cycle grabbed → stalled → blocklisted → regrabbed, and every cycle mints a fresh grabbed row. queue_busy() counted any grabbed row as work in flight, so an install with one perpetually-junk Wanted title could never self-update again.
The fix: queue_busy now takes the same stalled_timeout_minutes the retirement sweep uses. A grabbed row older than the timeout is dead weight — the sweep already blocklisted its release and will retire the row — not a download in flight, so it no longer defers the update. A fresh grab still defers; an importing row still defers however old (restarting mid-write is the damage the guard exists to prevent); an undatable row still defers.
Three regression tests, including the live shape. Full suite: 2287 passed, 3 skipped.
v1.4.1 — MiNERVA results survive scoring
ROMarr v1.4.1 — MiNERVA results now survive scoring.
Unknown size is not "too small" (#62). Found live on the v1.4.0 MiNERVA rollout: the archive's search API publishes no file sizes, so every release carried size=0 — and the size floor treated zero as a real measurement, rejecting all of them ("too small to be a Super Nintendo cartridge") however perfect the title match was. A live search found 11 Chrono Trigger releases and kept 0, all on this rule alone. Same latent bug for RSS-feed indexers reporting 0 for an enclosure with no length.
One character of logic: the floor now requires 0 < size. Zero means unknown; the release stands or falls on title, extension and platform evidence. A genuinely tiny release (1KB for a SNES cart) is still rejected — only the literal unknown is exempt. Two regression tests pin both sides of the line.
Full suite: 2284 passed, 3 skipped.
v1.4.0 — MiNERVA Archive + recursive presence scan
ROMarr v1.4.0 — MiNERVA Archive as a first-class source, and a collections presence scan that actually looks inside subfolders.
MiNERVA Archive indexer. Myrient shut down 31 March 2026; MiNERVA (minerva-archive.org) is the community continuation of its entire catalogue, distributed as collection torrents with a public, keyless JSON search API. New minerva indexer type speaks its latest surface: /v1/api/rom/search with the site's full filter vocabulary (group, console, region, tokenised) and 100-row offset pagination; caps() probes /v1/api/rom/filters and checks the response shape. Results collapse to one release per torrent (the archive carries [a8]/[a9] alternates that share a magnet), and report as private with a nominal seeder because the API publishes neither sizes nor seeder counts. Add it under Settings → Indexers with no API key — the public deployment works as-is.
Recursive collections presence scan. The collections import feature did not search recursively: _present_titles used iterdir() (one level), so a game filed in a subfolder or under Translations/ surfaced as its parent directory's name, the title read as missing, and a batch import queued a fresh download of a game already on disk. It now walks the tree (rglob): every file answers to its own stem and to every containing directory's name, so disc-set folders keep working at any depth. Includes an is_dir() guard — rglob on a missing directory yields nothing instead of raising, which would have silently swallowed the shelf fallback for server-backed libraries.
17 new tests (13 driver with live-API-shaped fixtures, 4 presence-scan). Full suite: 2282 passed, 3 skipped.
v1.3.2 — Prowlarr key fix + RomM 5.4 rescan
ROMarr v1.3.2 — the Prowlarr "Rejected the API key" bug fixed, and RomM 5.4 rescans work.
Indexer credential masking (#57, #60). Reported: add Prowlarr → Test ✓ → Save → Test again → "Rejected the API key". The key was never wrong. The indexer Save/Test routes merged credentials with the download-client merger, which has no spec for indexer types — so after Save the form's ******** mask was stored as the actual API key, and every later Test sent eight asterisks to Prowlarr. Now indexer routes merge against indexer secret fields, a placeholder with nothing stored is refused with a clear message, and torrentrss passkey no longer ships to the browser in clear text.
RomM 5.4 rescan (#58). RomM 5.4 refuses scan_library through the task runner, so post-import rescans silently stopped working. ROMarr now uses 5.4's POST /api/tasks/scan (narrowed to the imported platform), falls back to the task runner on older servers, and treats a 409 "scan already running" as deferred-not-failed with a bounded retry.
Suites: 2266 passed, 3 skipped (10 new secret-merge tests incl. HTTP-layer save→test round trip; 15 new RomM rescan tests).
If your Prowlarr indexer says "Rejected the API key" after updating: open it, retype the real key once, Save.
v1.3.1 — URLs without a scheme now work
ROMarr v1.3.1 — a URL typed without a scheme now works.
Peer URL scheme probing (#59). The live failure: a friend's public URL stored as romarr.ant1695.fyi made every Friends-page call die with MissingSchema before anything was sent — "could not reach" for what was really a missing https://.
- New
url_candidates()is the one rule for every URL a human typed: an explicithttp:///https://is honoured exactly; a scheme-less address is tried https first, then http; junk is refused with a message that says what to type. _call_friendprobes candidates in order and keeps the first that answers — an answered 401/500 ends the probe (a refusal or sick server is not "unreachable"); only connection failures fall through to the next scheme.- Normalized at birth too, so scheme-less URLs stop being stored:
Federation.url,add_romm,accept_peer, the live public_url refresh, and the RomM friend client at use (legacy rows normalized defensively on read).
16 new tests including the reported bug verbatim and probe-order assertions. Full suite: 2241 passed, 3 skipped.
v1.3.0 — Sign in with Authentik (OIDC)
ROMarr v1.3.0 — sign in with Authentik, or any OpenID Connect provider.
OIDC sign-in (#56). The login page gains a "Sign in with …" button. ROMarr becomes the relying party in an authorization-code + PKCE flow, so a directly-reachable install — no reverse proxy required — can authenticate people through an external IdP. Works with Authentik, Keycloak, Zitadel, Google, or anything else speaking OIDC.
- Configure it under Settings → General → Sign in with an identity provider (OIDC), or entirely in the environment (
ROMARR_OIDC_ISSUER,ROMARR_OIDC_CLIENT_ID,ROMARR_OIDC_CLIENT_SECRET,ROMARR_OIDC_GROUP, …). Endpoint discovery via/.well-known/openid-configuration, with manual URL overrides for IdPs behind path rewriters. The callback to register is<public_url>/oidc/callback. - Optional access control: a required group or an email allowlist. A missing
groupsclaim fails a group restriction rather than satisfying it. - Security: PKCE S256 always;
statechecked before any token exchange (CSRF);noncebinds the id_token to the login (replay); id_token validated claim-by-claim (iss/aud/exp/iat/azp) and signature-verified against the JWKS whencryptographyis installed. Without it, RS256 falls back to OIDC Core §3.1.3.7 channel trust over https only — an http issuer with no verifier is refused. Client secret stored write-only. Full trust model in SECURITY.md; open-path table now twelve routes. - This complements the existing forward-auth SSO (
ROMARR_AUTH=forward), which believes a proxy's headers; both can be understood as "SSO" but they answer different deployments.
41 new tests (unit + HTTP-layer: redirect cookies, state-mismatch refused pre-exchange, every claim-refusal path, HS256 verify/reject, no-crypto https fallback + http refusal, secret write-only). Full suite: 2225 passed, 3 skipped.
v1.2.1 — self-update hardening
ROMarr v1.2.1 — self-update hardening.
- The detached updater now installs dependencies into the interpreter that actually runs ROMarr (
sys.executable— the venv on a normal install). It previously picked systempython3via PATH, where pip refuses under PEP 668 ("externally-managed-environment"); a real update adding a dependency would have restarted into new code with a missing import. (#55) - A failed dependency install now rolls the checkout back (
git reset --hardto the pre-update commit) and exits without restarting — staying on the running old version beats coming up on new code that cannot import. (#55)
Both were found by running the self-update path end-to-end on a live git install and reading its log; the restart machinery itself was proven working in the same run.
Full suite: 2183 passed, 3 skipped.
v1.2.0 — self-update from GitHub, https-safe library covers
ROMarr v1.2.0 — self-update, and library covers that survive a reverse proxy.
Self-update system (#52). UpdateCheck no longer stops at "a newer version exists" — ROMarr can now apply updates from GitHub itself, behind three guards:
- Deferral — never restarts while the queue holds
grabbed/importingrows. - Detached apply — a
start_new_sessionscript outlives the restart:git fetch,git merge --ff-only(a diverged checkout refuses rather than merging),pip install, service restart; fail-stop and logged, with the pre-update commit recorded as a rollback handle. - Honest install detection — a git checkout self-applies; a container cannot (the image owns the code) and is given the exact host
docker pull/ compose commands instead of a fake success. - Channels:
stable(latest release tag) andnightly(origin/main). ROM Hub plugins update through the Hub's own install path, per-plugin isolated. - New System → Update page,
GET /api/v1/update, andApplyUpdate/PluginUpdatecommands. All auto-apply defaults are off — opt-in.
Library covers over https (#49). A RomM reached internally over http:// behind an https proxy produced blank posters (browsers block http images on an https page). http:// covers now route through a same-origin /api/v1/asset relay — the browser fetches ROMarr, ROMarr fetches the library server-side. Not an open proxy: the URL's origin must match a configured library or it's refused without a fetch. https:// covers stay direct.
Update check robustness (#54). A GitHub 403/429 is now reported as the rate limit it is (60/hr unauthenticated, shared per NAT IP) rather than "could not reach github.com". GITHUB_TOKEN/GH_TOKEN is sent as a Bearer header, lifting the limit to 5,000/hr.
Dashboard JS regression guard (#51). Every inline <script> block in the rendered page is now extracted and run through a real node --check in CI, plus a structural scan for the quote-backslash line-continuation that killed the dashboard in 1.1.0 (#48/#47). This failure class can no longer ship green.
Full suite: 2182 passed, 3 skipped.