Skip to content

Sphexi/ROMulus

Repository files navigation

ROMulus

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/.


Installation (portable, Windows)

The easiest way to run ROMulus on Windows is the portable ZIP:

  1. Download romulus-windows-x64.zip from the Releases page.
  2. Extract it anywhere — C:\Tools\ROMulus\, a USB stick, wherever. No installer, no registry entry, nothing to uninstall.
  3. 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.


Installation (from source)

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 romulus

For 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).


Quick start

The first time you run ROMulus the window is empty. The recommended workflow:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Find Covers (toolbar) — separate workflow with two independent checkboxes: "Search for local covers" (walks the library tree for .png/.jpg files matching enrolled ROMs) and "Search online for covers" (libretro-thumbnails fetch). Either, neither, or both per run.
  6. Organize (toolbar) — previews proposed library cleanups (alias folder merges, canonical renames, duplicate removal). Every action is reviewed and approved before anything moves.
  7. 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.
  8. 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.
  9. 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.
  10. Tools → Clean Missing Entries — removes tombstoned rows the user is confident are gone for good. Cascades via ON DELETE CASCADE to dependent hashes / metadata / covers / collection_roms / dest_inventory rows 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.


Settings

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).


Troubleshooting

Library & scanning

"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 & identification

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....

Enrichment & covers

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.

Export & Sync

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.

Logging & operations

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).


Documentation

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

Contributing & development

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.


License

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.

About

Cross-platform ROM manager in the style of Wii Backup Manager

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors