A self-hosted web app for browsing and downloading ROMs you legally own. Django + SvelteKit, running under Docker Compose — a web port of romgi, a Flutter/Android ROM downloader.
Downloads run over plain HTTP, BitTorrent (via qBittorrent), or a debrid provider (Real-Debrid/TorBox), with live progress in the browser, optional archive extraction, Internet Archive login for restricted content, and optional game metadata enrichment (ScreenScraper/SteamGridDB).
This project ships no ROMs and hosts no files. It indexes publicly available sources and drives downloads on your own hardware, using your own accounts and credentials. What you download, and whether you're entitled to it, is between you and the law where you live.
| Browse | Entry detail |
|---|---|
![]() |
![]() |
| Downloads | ROM sets |
|---|---|
![]() |
![]() |
- Catalog browse and search across multiple ROM sources, filterable by platform and region.
- Three download paths — direct HTTP, BitTorrent through a bundled qBittorrent daemon, or a debrid provider — with automatic link ranking and failover between them.
- Live progress over WebSocket, so a running download updates in place rather than on refresh.
- ROM sets — browse whole published romsets on archive.org by emulator (FBNeo, MAME, …) and fetch one as a unit, instead of a file at a time. Per-file selection, so a 5GB file you don't want is never transferred.
- BIOS files — the system files an emulator won't boot
without, from archive.org over plain HTTP into
bios/<platform>/, with archive.org's published md5 verified on every file. - Metadata enrichment from ScreenScraper/SteamGridDB, with box art and a local cache.
- Favorites and recently-viewed, per user.
- Invite-only multi-user auth — no open signup, per-account lockout, rotating refresh tokens, and a credential vault encrypted at rest.
- Internet Archive login for items in the
loggedincollection.
- Backend: Django 5.1 + Django Ninja (REST API) + Django Channels (WebSocket progress) + Celery (background work: ingestion, downloads, torrents, credential/metadata calls) + PostgreSQL + Redis.
- Frontend: SvelteKit (Svelte 5, CSR-only) + FlowbiteSvelte + Tailwind CSS v4.
- Torrents: a qBittorrent-nox daemon, driven via its Web API. Both the
per-ROM downloads and the ROM-set downloads drive it, and qBittorrent
dedupes by infohash — so
apps/torrents/ownership.pyis the single place that decides whether a torrent is still wanted before anything stops or deletes it. - Catalog ingestion: a vendored copy of the original app's Python ETL
pipeline (
backend/apps/ingestion/pipeline/), writing into Postgres instead of a SQLite file.
A per-app breakdown of the tree is in docs/development.md.
Requires Docker and Docker Compose.
cp .env.example .env # where files land on the host
cp backend/.env.example backend/.env # application configSet ROM_LIBRARY_HOST_PATH in .env to an absolute path to an existing
folder — it's where downloaded ROM sets land, it has no default, and
docker compose up refuses to start without it. The rest of that file is
optional; see docs/installation.md.
Set two values in backend/.env:
# SECRET_KEY
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
# ENCRYPTION_KEY
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"If either generated value contains a
$, escape it as$$— Docker Compose interpolates.envvalues and will otherwise corrupt the key silently. See docs/installation.md.
docker compose up --build| Service | URL |
|---|---|
| Frontend | http://localhost:5174 |
| Backend API | http://localhost:8001/api |
| Backend admin | http://localhost:8001/admin |
| qBittorrent WebUI | http://localhost:8080 |
Registration is invite-only, so the first account comes from the shell:
docker compose exec django python manage.py createsuperuser
# Prints a signup link to send to the new user.
docker compose exec django python manage.py createinvite --email someone@example.comThe catalog starts empty — ingest a source to fill it:
docker compose exec django python manage.py ingest_catalog --sources mariocubeOne more setup step matters before torrents work — qBittorrent's first-boot password — in docs/installation.md.
| Installation | Full configuration, using existing Postgres/Redis/qBittorrent servers, first account, loading a catalog |
| Development | Running backend/frontend on the host, VS Code debug configs, tests, code layout |
| ROM sets | How whole-romset downloads work, and the disk-space rules that govern them |
| BIOS files | Where BIOS files come from, where they land, and why they don't use BitTorrent |
| Deployment | Production settings and the auth model |
| Known issues | What's unverified, deliberately unimplemented, or worth a second look before relying on this in production |
KNOWN_ISSUES.md is worth reading first if you're evaluating this — several
integrations were built as faithful ports but never observed succeeding
against the real service, and it says exactly which.
Ported from caprado/romgi (MIT License, © 2025 Christian Prado) — the catalog ingestion pipeline, download state machine, link-ranking/failover logic, torrent handling, debrid resolution, Internet Archive login flow, and metadata provider integrations are all adapted from that project's Dart/Kotlin/Python source, reimplemented here in Python (Django/Celery) and TypeScript (SvelteKit). The original project isn't vendored in this repo (it's reference material, not a runtime dependency) — see its GitHub page for the original Android app.
MIT — see LICENSE.



