Skip to content

Releases: BlizzHacker/romarr

v1.6.0 — real RetroArch (NCI)

Choose a tag to compare

@BlizzHacker BlizzHacker released this 15 Oct 05:28

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.

  • RetroArchNCI duck-types the existing stream-server slot and rides StreamSources between 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_PARAM and raises on anything else — LOAD_CORE can 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_CORES is post-1.19.1 and deliberately unused), so: a loaded game's system_id proves 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_STATUS parsing verified against RetroArch v1.19.1's command.c and 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

Choose a tag to compare

@BlizzHacker BlizzHacker released this 15 Oct 02:57

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's torrent capability 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.search gives 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

Choose a tag to compare

@BlizzHacker BlizzHacker released this 15 Oct 01:51

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

Choose a tag to compare

@BlizzHacker BlizzHacker released this 15 Oct 01:11

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

Choose a tag to compare

@BlizzHacker BlizzHacker released this 15 Oct 00:38

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

Choose a tag to compare

@BlizzHacker BlizzHacker released this 14 Oct 22:39

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

Choose a tag to compare

@BlizzHacker BlizzHacker released this 14 Oct 16:33

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 explicit http:///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_friend probes 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)

Choose a tag to compare

@BlizzHacker BlizzHacker released this 13 Oct 19:59

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 groups claim fails a group restriction rather than satisfying it.
  • Security: PKCE S256 always; state checked before any token exchange (CSRF); nonce binds the id_token to the login (replay); id_token validated claim-by-claim (iss/aud/exp/iat/azp) and signature-verified against the JWKS when cryptography is 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

Choose a tag to compare

@BlizzHacker BlizzHacker released this 13 Oct 18:47

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 system python3 via 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 --hard to 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

Choose a tag to compare

@BlizzHacker BlizzHacker released this 13 Oct 16:40
ba38e74

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/importing rows.
  • Detached apply — a start_new_session script 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) and nightly (origin/main). ROM Hub plugins update through the Hub's own install path, per-plugin isolated.
  • New System → Update page, GET /api/v1/update, and ApplyUpdate/PluginUpdate commands. 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.