819811b155 feat(converto): library format settings and "Download as" on the game page (#5000)
* feat(converto): integrate rom-converto for title id extraction and download conversion

* feat(converto): add conversion settings UI with per-platform format policies

* fix(converto): read conversion targets from the backend and translate the settings page

* fix(docker): ship rom-converto in the production image

Only the dev Dockerfile installed the binary, so the integration could
never enable on a released image. Fetch the same pinned, checksummed
musl build in a dedicated stage and copy it next to RAHasher.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(converto): skip the update check and batch info calls

The CLI asks api.github.com for a newer release on every run unless
told not to, so pass --no-update-check on each invocation. Inspect a
rom's files in one `info --paths-file` run instead of one subprocess
per file, and read the inspectable extensions from the capabilities
manifest so scans skip files info cannot recognize.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(scan): keep sigil authoritative for title ids

rom-converto ran first and overrode the ids sigil read. Sigil now runs
first and records its per-file ids; rom-converto only fills files sigil
left unset, on new or changed files, in one batch. Restore the base-id
selection for sigil extractions on every platform, as on master.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(downloads): make conversion opt-in and bound the cache

The download endpoint returned a converted file to every client once a
platform had a target, including the browser player and apps that check
hashes against RomFile. Conversion now needs ?converted=true.

A pre-warmed copy is served whatever its size; the in-request size cap
only gates starting a conversion. ROM_CONVERTO_CACHE_MAX_SIZE_GB
(default 20, 0 unbounded) caps the cache: conversions and the pre-warm
task stop when it is full, and cleanup evicts the least recently served
copies.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(types): regenerate frontend types for the converto schema

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(converto): address code-review findings (pr-ready step 2)

- Fall back to rom-converto's stored ids only when it ran this pass, in
  disc order, so a rescan that skipped extraction keeps sigil's save target
- Persist per-file title ids that extraction writes onto reused rows
- Only authenticated callers may start an in-request conversion; past a
  240s deadline serve the original and let the conversion finish into the
  cache instead of nginx returning 504
- Treat a probe OSError or timeout as a failed probe; kill the subprocess
  when the caller is cancelled
- Give the pre-warm task the scan timeout and report progress in the
  ConversionStats shape the task UI reads
- Reuse zip_cache constants, walk the cache off the event loop, tolerate
  files vanishing mid-cleanup, and trim long comments

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(converto): simplify conversion code (pr-ready step 3)

- Move the converted-download policy (target lookup, cached-first, sync
  size cap, deadline) from the endpoint into utils/conversion_cache and
  share conversion_target with the pre-warm task
- Share one path lookup between get_cached_converted and get_or_convert;
  the pre-warm walks the cache once and tracks a running byte budget
  instead of walking it twice per rom
- One normalize_platform_formats beside TARGETS_BY_PLATFORM for the
  config loader (via _validated_platform_map) and the settings endpoint;
  update_converto_settings takes a ConvertoConfig and is written back
  with dataclasses.asdict
- read_infos owns its never-raise contract, so the scan drops its blanket
  except; drop the unused RomConvertoInfo.kind and error attributes
- Sort files for the rom-converto identity only when it is used; stat
  each cached file once during cleanup

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(converto): trim comments (pr-ready step 4)

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(converto): convert on first download instead of pre-warming

A library pre-warm fought the TTL: cleanup dropped every copy nobody
downloaded within a day. Remove the Convert library task and convert on
demand only. Files over ROM_CONVERTO_MAX_SYNC_SIZE_MB now start their
conversion in the background while the original is served, so the next
download within the TTL gets the converted copy at any size.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(converto): drop decrypt and encrypt download targets

Downloads now only change a rom's container or compression (CHD, RVZ,
CSO, NSZ, ...); serving decrypted files is left for a separate decision.
NDS and PS3 have no remaining targets. Also note that the conversion
concurrency limit applies per process.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(converto): move the cache limits into converto.*

cache_max_size_gb and max_sync_size_mb join cache_ttl_hours in
config.yml and PUT /api/config/converto_settings, replacing the
ROM_CONVERTO_CACHE_MAX_SIZE_GB and ROM_CONVERTO_MAX_SYNC_SIZE_MB env
vars, so every eviction knob is edited in one place at runtime. The
loader takes its defaults from ConvertoConfig and one table holds each
integer's minimum for both the loader and the payload.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(converto): lock conversions with flock instead of a sentinel

A conversion now holds a non-blocking flock on its key dir, which the
kernel releases if the worker dies. That replaces the .partial sentinel,
its 6-hour staleness guess (which a long ROM_CONVERTO_TIMEOUT could
outlive), and the rename-aside takeover. Cleanup skips locked dirs, and
partial output gets a hidden name so it never counts as a served copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* test(converto): parametrize repeated test cases

Collapse the per-platform info parsing, operation resolution and
converto_active cases into parametrized tests, so a new platform or
target is one row.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(scan): run rom-converto first with sigil as the fallback

rom-converto reads each rom's new or changed files first, in one batch,
and its title ids win. sigil still reads what it can, since it alone
provides save targets and Switch content types, and fills the ids
rom-converto left unset.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix(converto): edit the cache size limits on the settings page

* feat(converto): store per-file metadata read by rom-converto during scans

* fix(converto): show the labels of the cache settings fields

* fix(scan): keep the stored save target when sigil reads nothing

* fix(converto): read Xbox 360 disc ids and Wii title versions, store content type as text

* feat(converto): save file icons, banners and backgrounds read by rom-converto

* chore(converto): bump rom-converto to v0.23.0

* fix(converto): address review findings in the conversion cache, scan metadata and settings

* refactor(converto): move per-file metadata out of the conversion PR

Scan-time metadata (title ids, the 17 rom_files columns, artwork and
converto.scan_metadata) moves to its own PR so its columns land with the
UI that reads them. This PR keeps conversion only.

The conversion cache no longer has uncompressed_size_bytes to budget
with, so a conversion to an uncompressed target reserves a fixed
multiple of its input and the cleanup pass after it evicts any overshoot.

Co-Authored-By: DevYukine <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017BuuZVVtVPNLRaCjTN9bgQ

* feat(converto): let the client pick the download format with ?format=

`?format=zso,iso` lists the formats a client can read. The stored file is
served whenever its format is listed, even with conversion off, so most
emulator clients never trigger a conversion. Otherwise the first listed
format with a cached copy is served, then the first one rom-converto can
produce.

A conversion that outlasts a short wait answers 202 with Retry-After
instead of falling back to the original, so a URL never serves two
different files and a client never gets bytes it can't read. 406 means
no listed format can be served. HEAD reports the same without starting
a conversion. This replaces `?converted=true`, the per-platform download
target and `max_sync_size_mb`; `cache_ttl_hours` stays config.yml-only.

Co-Authored-By: DevYukine <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017BuuZVVtVPNLRaCjTN9bgQ

* feat(converto): convert the library in place with a manual task

`converto.platform_formats` now names the format a platform's library is
stored in, and the new "Convert library" task brings every matched rom
to it. Each output is staged in a `.romm_tmp_` directory beside the
original, moved into place, and only then are the originals (and a cue's
tracks) deleted. Single-file roms take the new name, folder roms keep
theirs and get their .m3u entries rewritten, and the file rows refresh,
so a rom keeps its id, saves and collections.

Only lossless conversions qualify, so xbox (xiso trims the dump) has no
library format. Unmatched roms are skipped because a converted file no
longer hash-matches a DAT. `/api/config` now carries the library targets
and, per input extension, the formats a download can be converted to.

Co-Authored-By: DevYukine <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017BuuZVVtVPNLRaCjTN9bgQ

* feat(converto): library format settings and "Download as" on the game page

The settings page follows the reworked backend: per-platform formats are
now the library format the "Convert library" task stores matched games
in ("Leave as is" keeps a platform untouched), the scan metadata, cache
TTL and max-sync fields are gone, and a "Convert library now" button
starts the task behind a typed confirmation, since it deletes the
originals.

The game's More menu offers "Download as <format>" for a single-file rom
when download conversion is on, from the per-extension table
`/api/config` exposes. A one-byte ranged GET asks for the format, the UI
polls through 202s while it converts, then downloads it; a 406 shows an
error.

Co-Authored-By: DevYukine <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017BuuZVVtVPNLRaCjTN9bgQ

* fix(converto): address code-review findings (pr-ready step 2)

Guard the conversion settings route, add it to the user menu, ignore a
non-positive Retry-After, drop duplicate Download as clicks, name the game
in the persisted error, and tidy the settings page (display-name sort,
useUnloadGuard, decimal cache sizes, save snapshot).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(settings): share the config alerts and save bar (pr-ready step 3)

Extract ConfigFileAlerts and SettingsSaveBar from the three settings
views that copied them, move ScanSettings onto useUnloadGuard, align the
retry slot, and flatten downloadFormats, getDownloadFormats and the
format poll loop.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(converto): trim comments (pr-ready step 4)

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(converto): guard destructive tasks and serve download formats per rom

- TaskSpec gains destructive and single_instance. The Tasks list asks for a
  typed confirmation before a destructive task, and POST /tasks/run answers
  409 while a single-instance task is queued or running.
- convert_library is both, and only runnable with ROM_CONVERTO_ENABLED.
- The heartbeat reports CONVERTO.ENABLED, which hides Conversion settings
  from the sidebar and user menu and warns on the page.
- DetailedRomSchema.download_formats lists what the caller can convert the
  rom's single file to, resolved by the backend's own operation table and
  empty for visitors who can't start a conversion. It replaces
  CONVERTO_DOWNLOAD_FORMATS in /api/config and the frontend's copy of the
  extension matching.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Yukine <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: DevYukine <[email protected]>
2026-10-01 21:46:29 -04:00
2026-10-01 19:17:30 -04:00
2026-04-15 19:20:17 -04:00
2024-05-21 19:22:14 -04:00
2024-02-11 16:41:36 +01:00
2024-07-09 13:43:14 +02:00
2026-10-01 19:17:30 -04:00

romm logo

A beautiful, powerful, self-hosted ROM manager and player.

Trendshift hackernews badge
selfh.st badge Discord

Overview

Scan, enrich, browse and play your ROM collection from one beautiful & free self-hosted app. Metadata from 10+ providers, save sync across your devices, and support for over 400 platforms. RomM is a must-have for anyone who plays on emulators.

Browsing a library and playing a game in RomM

Features

Installation

The easiest way to start is with the Quick Start Guide in the docs. If you're having trouble with setup, please review the page for troubleshooting steps.

Prefer a hosted deploy over wiring up the stack yourself?

Deploy on Hostinger
Starting at $6.49/mo · Setup guide

Deployment and infrastructure support for this install is handled by Hostinger. Prices are indicative and may not reflect current offerings. Signups through the link above send a share of the revenue back to us, which helps fund the project.

Official apps

Playnite libraryPlaynite logo Playnite plugin

Effortlessly integrate your retro game collection into Playnite, the open-source game library manager that provides a unified interface for all of your games on PC.

Windows Desktop QR pairing

Install  by @gantoine


Argosy libraryArgosy logo Argosy

Sync your library, download games on demand and track your achievements, from a gamepad-first interface designed for Anbernic, Retroid Pocket, Odin, and similar devices.

Android Handhelds QR pairing Save sync

Download  by @tmgast


Grout multi-selectGrout logo Grout

A lightweight client for your favorite handheld custom firmwares*. Download games, box art and BIOS files wirelessly, and sync your saves automatically as you play.

Linux Handhelds Save sync

Quick start  by @BrandonKowalski

*Supports Allium, Batocera, Knulli, MinUI, muOS, NextUI, Onion, ROCKNIX, Spruce and TrimUI.

Community

Here are some cool projects maintained by members of our community. Please note that our team does not regularly review their source code.

Mobile

Desktop

Handhelds

Other

Support the project

Consider supporting this project on Open Collective. Funds cover hosting, development, and maintenance, and we pass some along to community projects to nurture a healthy ecosystem of apps.

Open Collective

Our Friends

Here are a few projects that we think you might like:

  • Lutris: Open gaming platform for Linux
  • Cocoon: Controller-first Android frontend for retro handhelds
  • muOS: Custom firmware for Anbernic and other handhelds
  • RetroDECK: Retro gaming on SteamOS and Linux
  • Steam ROM Manager: An app for managing ROMs in Steam
  • Hasheous: Hash lookup service that maps ROMs to game metadata
  • Gaseous: Another ROM manager with web-based emulator
  • Playmatch: Microservice for matching ROM hashes to games
  • EmulatorJS: An embeddable, browser-based emulator
  • PortMaster: Native game ports for Linux handhelds
  • MelonLoader: Universal mod loader for Unity games
  • NextUI: MinUI-based custom firmware for TrimUI handhelds
  • RetroAchievements: Achievements for retro games

Supported By

These companies support us by providing their tools for free:

Greptile Socket
S
Description
A beautiful, powerful, self-hosted ROM manager and player.
Readme AGPL-3.0
466 MiB
Languages
Python 55.8%
Vue 24.5%
TypeScript 18.8%
CSS 0.4%
JavaScript 0.2%
Other 0.3%