A local-first desktop ROM collection manager for retro game consoles. Scan, identify, enrich with metadata + cover art, organize, and sync your collection to whatever device you actually play on — Anbernic handhelds, Batocera setups, MiSTer FPGAs, Analogue Pocket, RetroPie, muOS, Onion OS.
No server. No cloud account. No external services to keep running. SQLite + files on disk, nothing else.
Project status: v0.4.0 (in development). The full scan → identify → enrich → organize → import / export / sync pipeline works end-to-end. v0.4.0 completes a strict 1:1 rom↔game data-model refactor: every ROM file is its own identity unit with its own metadata, covers, and collection memberships. See CHANGELOG.md for the per-release breakdown.
License: Apache License 2.0.
Built with LLM assistance. Architecture, API choices, and design rules
are owned by the human maintainer; most of the implementation typing was
driven by Claude Code.
Commits carry Co-Authored-By: Claude Opus ... trailers and the historic
work breakdown lives under docs/sessions/.
The easiest way to run ROMulus on Windows is the portable ZIP:
- Download
romulus-windows-x64.zipfrom the Releases page. - Extract it anywhere —
C:\Tools\ROMulus\, a USB stick, wherever. No installer, no registry entry, nothing to uninstall. - Double-click
romulus.exe.
After first launch the folder looks like this:
ROMulus\
romulus.exe (single self-contained binary)
profiles\*.yaml (destination profiles — edit freely)
systems\*.yaml (system registry — drop in extra YAMLs to extend)
dats\*.dat (bundled No-Intro DAT files)
gamedb\*.json (bundled GameDB metadata snapshots)
libretro-metadat\ (bundled libretro-database metadata DATs)
data\ (romulus.db + covers cache — runtime state)
logs\ (rotating log file)
Backup = zip the folder. Move to another PC = copy the folder. Everything is local; nothing else on your machine is touched.
The ROMULUS_DATA_DIR env var pins the data directory anywhere — useful
if you want the exe on a fast SSD but the SQLite DB and cover cache on a
roomier drive.
Required for macOS / Linux today, since the portable build is Windows-only.
Prerequisites: Python 3.12+, Git, a desktop environment that can run Qt 6 (Windows 10/11, macOS 12+, or a recent Linux distro with X11/Wayland).
git clone https://github.com/Sphexi/ROMulous.git
cd ROMulous
python -m venv .venv
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
# macOS / Linux
source .venv/bin/activate
pip install -e .
# Launch
python -m romulusFor development (adds pytest + ruff + PyInstaller): pip install -e ".[dev]".
On Linux you may need to apt-install the X11 / Wayland / OpenGL libraries
PySide6 wheels link against — see .github/workflows/ci.yml for the full
list of Debian/Ubuntu packages, although note that CI runs on
windows-latest now (see docs/architecture.md for
why).
The first time you run ROMulus the window is empty. The recommended workflow:
- File → Open Library... — point ROMulus at the root of your ROM folder. If you've previously scanned a different folder, ROMulus prompts to wipe the stale entries; ROMulus treats one library folder at a time as the source of truth.
- Quick Scan (toolbar) — walks the library, detects which console
each ROM belongs to, parses filenames for region/revision/disc/hack
flags. Seconds to a few minutes for tens of thousands of files. No
hashing. Files that vanished since the last scan are tombstoned (kept
in the DB with
missing=1) rather than dropped, so enrichment survives a temporarily-unmounted share. Right-click a system in the sidebar to scope the scan to just that system. - Heavy Scan (toolbar) — computes SHA-1/CRC32 with header stripping and matches against bundled No-Intro DATs for canonical naming. Re-runs of unchanged files are nearly free thanks to the hash cache.
- Enrich Metadata (toolbar) — fills in genre / developer / publisher / release date / players / rating from a local-first chain of sources. The pre-run dialog has a "Also try online metadata sources" checkbox; uncheck it to run completely offline.
- Find Covers (toolbar) — separate workflow with two independent
checkboxes: "Search for local covers" (walks the library tree for
.png/.jpgfiles matching enrolled ROMs) and "Search online for covers" (libretro-thumbnails fetch). Either, neither, or both per run. - Organize (toolbar) — previews proposed library cleanups (alias folder merges, canonical renames, duplicate removal). Every action is reviewed and approved before anything moves.
- Import ROMs (toolbar / Tools menu) — walks a staging folder (Downloads, USB stick, mounted archive), identifies every file via the same Quick + Heavy pipeline, surfaces three levels of duplicate detection (path / filename / hash), and atomically copies or moves the approved files into the current library.
- Export / Sync (toolbar) — pick a destination profile (Batocera, RetroPie, MiSTer, Anbernic, etc.), pick a target folder, and run a one-shot Export or a Sync with one of five modes (push merge/mirror/wipe, pull merge, two-way). Every sync produces a preview with per-action counts; destructive modes require a double-confirm. After Apply finishes a per-system summary dialog breaks down what each system contributed (copied / bytes / covers refreshed / unsupported / refused / errors). To push fresh artwork without re-copying ROMs, uncheck Include ROMs in the Export Options.
- Tools → Verify Library — reverse-direction integrity check. Walks the database and verifies every row against disk; surfaces four buckets (missing-on-disk, outside-current-library, flagged-but- present, size/mtime drift) and lets you fix each one per-bucket.
- Tools → Clean Missing Entries — removes tombstoned rows the user
is confident are gone for good. Cascades via
ON DELETE CASCADEto dependenthashes/metadata/covers/collection_roms/dest_inventoryrows automatically.
Right-click a game in the table for: Add to Favorites / Add to Collection / Heavy Scan this game / Enrich this game / Find covers for this game / Reveal in Explorer / Delete this ROM. Right-click a system in the sidebar for a system-scoped Quick Scan / Heavy Scan / Enrich / Find Covers.
Click a game to see its detail panel — cover art with prev/next cycling, platform logo, key/value metadata grid, and description.
Right-click a group header in any preview dialog (Organize / Sync / Verify Library) for tri-state bulk toggle — flip an entire bucket without per-row clicking on multi-thousand-row plans.
Everything is editable through File → Settings.... There is no config file to hand-edit; configuration lives in the SQLite database.
Common tabs:
- General — library path, theme, default view, log level.
- DATs — DAT folders. Multiple folders supported; rescanned at startup.
- Metadata — ScreenScraper credentials (optional), TheGamesDB API key (optional). Both have Test connection buttons.
- Scan — worker thread count for Heavy Scan.
- Diagnostics — install dir, data dir, log path. Copy these into bug reports.
The ROMULUS_LOG_LEVEL environment variable (DEBUG / INFO / WARNING
/ ERROR) overrides the saved log level at startup — useful for one-off
diagnostics without touching Settings. The log file is at
<install_dir>/logs/romulus.log (rotating, 5 MB × 3 backups).
"No library configured" when I click Quick Scan. Use File → Open Library... first to point ROMulus at the root of your ROM folder.
Quick Scan finished but the game table is empty. The scanner only
shows files it could place in a known system folder. See
docs/ROM-FORMATS-REFERENCE.md for the folder-name aliases each system
accepts. Either rename your folder to match (e.g. SNES, snes,
Super Nintendo) or add a YAML entry to systems/ and restart.
Quick Scan shows "N missing" in the status bar. Files the scanner expected to find weren't on disk this time. Reconnect the drive / remount the share and re-scan — tombstoned rows un-tombstone automatically. If they're really gone, Tools → Clean Missing Entries… removes them.
Switching libraries shows a "N entries from previous libraries will be removed" prompt. ROMulus treats one library folder at a time as the source of truth. Pick "Yes" to drop the previous library's rows; pick "No" to back out of the switch.
The DB has more ROM rows than my disk has files. Run Tools → Verify Library. It walks every row in the database and classifies mismatches into four buckets — missing on disk (not yet flagged), pointing outside the current library root, wrongly flagged missing when the file is back, and rows whose stored size/mtime have drifted from disk. Each bucket can be applied independently.
Heavy Scan completes but the dialog says "cache up to date". Quick Scan must run first to detect file changes; Heavy Scan only hashes ROMs the cache flags as new or modified. If your library actually has new files, run Quick Scan and then Heavy Scan again.
Heavy Scan completes but nothing got matched. Check that the relevant
DAT file is in dats/ — bundled DATs cover ~80 systems but not
everything. Add user DATs via Settings → DATs → Add folder....
Cover art doesn't appear after Find Covers. libretro-thumbnails keys covers by the canonical No-Intro game name. A ROM that didn't pick up a canonical name (no header, no hash match, no DAT entry) won't fetch online covers cleanly. Right-click → Heavy Scan (this game's ROMs) to upgrade the identifier confidence, then re-run Find Covers.
I want to push fresh covers without re-syncing the whole library.
Open Export / Sync, uncheck Include ROMs at the top of Options,
click Export. The ROM copy loop is skipped entirely; only gamelist.xml
and the cover folders are touched. copy_artwork does a size + mtime
compare so only the covers that actually changed get re-pushed.
ScreenScraper "Test connection" says invalid even though my credentials work on the website. ScreenScraper occasionally returns non-JSON HTML during maintenance windows; the test treats that as failure. Retry after a few minutes.
Some systems are marked checked but the destination has no folder for
them. Each destination profile knows which systems the target device
actually supports. The Anbernic RGLauncher profile, for instance,
explicitly marks home computers (Amiga, C64, ZX Spectrum, Atari ST,
Amstrad CPC) as supported: false because the stock launcher can't
display them even if files are copied. The Export per-system summary
shows these as "Unsupported" so you can see exactly how many files were
skipped per system. If you want them on the device anyway, switch to a
launcher that supports those systems (ES-DE Android, Daijisho) and
either pick or write a matching profile.
Sync per-system summary shows "Refused" errors on MAME / Mega Drive
files. This is the security guard refusing to overwrite a pre-existing
destination file whose size differs from the source. Almost always means
you have two ROMs in the library with the same filename (e.g. multiple
MAME romset versions of 1941.zip) and both want to land at the same
destination path. The first wins; the second is refused. Dedupe in the
library with Organize → Delete Duplicate before re-running.
Sync preview shows everything as "identical" or nothing matches. The
sync engine's identity matcher requires the destination file's folder to
map to the same system as the local ROM. If your destination uses
non-standard folder names, the profile YAML's systems.<id>.folder field
needs to match what's actually on disk.
Export progress bar sits at 100% for minutes. That's the sidecar
pass (artwork + gamelist.xml) running after the ROM copy completes.
v0.3.0 added explicit phase-2 progress ticks — the bar now rescales to
the system count and the label switches to "Refreshing sidecars:
<system_id>" so you can see motion. If you're on an older build,
update.
Sync froze during "Computing diff…" or right after the dest scan.
Fixed in v0.3.0. The pre-fix bug was an O(N·M) fuzzy-key scan during
plan-build on large libraries (38K × 17K ≈ ~600M regex calls). The
post-fix version pre-indexes the destination by (fuzzy_key, region, system_id) and runs build_plan on a worker thread with a "Computing
diff…" progress dialog. If you still see a freeze, grep
logs/romulus.log for build_plan: start and build_plan: complete
to see whether the diff phase finished.
DEBUG log level in Settings looks like nothing happens. Set
ROMULUS_LOG_LEVEL=DEBUG in the environment before launching — the env
var beats the Settings value on startup.
The app froze during a long operation. The end of Quick Scan now disables the Cancel button while it finalises the DB (the "Marking missing entries…" / "Finalising scan history…" labels are post-walk phases that can't be safely cancelled; the old "Linking ROMs to games…" phase is gone in v0.4.0). The Clean Missing Entries and Verify Library apply phases also disable cancel during chunked deletes. For other freezes, please open an issue with the worker name, library size, and the last log line.
Starting a second copy of ROMulus errors out about the log file. Only
one instance can hold logs/romulus.log at a time. Close the first
instance, or set ROMULUS_DATA_DIR to a different folder for the second.
On Linux, python -m romulus crashes with a missing-library import
error. PySide6 wheels link against a long list of X11 / Wayland /
OpenGL libraries. Install them via your package manager — ci.yml has
the Debian/Ubuntu list (even though CI itself now runs on
windows-latest).
| Doc | What's in it |
|---|---|
| docs/architecture.md | Architecture, design rules, schema, config reference, destination profile format, packaging, limitations |
| docs/TECHNICAL_PLAN.md | Full implementation spec — schema details, identifier pipeline, every subsystem in depth |
| docs/sync-design.md | Destination sync engine spec (modes, identity matcher, dest_inventory, sync_plans, perf notes) |
| docs/import-design.md | Import ROMs feature reference (shipped) |
| docs/strict-1to1-design.md | v0.4.0 strict 1:1 rom↔game data-model design doc |
| docs/forking-with-claude-code.md | How to fork this repo and continue building it with Claude Code |
| docs/ROM-FORMATS-REFERENCE.md | Extension tables, naming conventions, folder aliases |
| docs/ROM-DEDUP-METHODOLOGY.md | Three-layer identification pipeline methodology |
| docs/CREDITS.md | Upstream services, open-source libraries, ROM-preservation projects, console/launcher targets, artwork sources |
| CHANGELOG.md | Per-release feature + fix history |
| CLAUDE.md | Project rules and session checklist for LLM-assisted work |
git clone https://github.com/Sphexi/ROMulous.git
cd ROMulous
python -m venv .venv
.venv\Scripts\Activate.ps1 # Windows
pip install -e ".[dev]"
# Run tests + lint
.venv/Scripts/python.exe -m pytest
.venv/Scripts/python.exe -m ruff check src/ tests/Current state: 1,015 tests passing, 8 skipped (7 platform-specific
cover-UI skips + 1 POSIX chmod skip on Windows). CI runs on
windows-latest.
See docs/architecture.md for code-style notes, the project layout, the worker / threading model, and the design rules that govern what changes are in-scope.
ROMulus is distributed under the Apache License 2.0. All code in this repository is original work authored by the human maintainer with LLM assistance.
Third-party services and data sources retain their own licenses and usage terms — see docs/CREDITS.md for the full list.