Pick the games you want, fetch only those, and keep a verified MAME library current across releases.
3.4K
A MAME library manager in the shape of Sonarr/Radarr: you declare what you want, it acquires only that, keeps it categorised, and moves it forward a version at a time without re-downloading the world.
Pleasuredome publishes MAME as two enormous torrents — 163 GB of ROMs and 1.13 TB of CHDs. Most of it is machines that do not work, are prototypes, are mechanical, or are in genres you would never play. Marquee works out which files you actually want, tells the download client to fetch only those, and builds a categorised library out of the result.
| Published | Fetched | |
|---|---|---|
| MAME 0.289 ROMs (non-merged) | 163.2 GB | 51.1 GB |
| MAME 0.288 CHDs (merged) | 1.127 TB | 292.5 GB |
| Total | 1.290 TB | 343.5 GB |
Upgrading is cheaper still. Moving a 0.282 library to 0.289 — seven releases — changes 244 machines and adds 877, which is 4.46 GB rather than 163 GB.
Those are measured figures, not estimates. See docs/ARCHITECTURE.md for how they are arrived at and why the approach works.
It is built for EmulationStation systems like Batocera, RecalBox and RetroPie, but works anywhere you need a categorised MAME set.
gamelist.xml that keeps EmulationStation's favourites and play counts; title
screens from libretro-thumbnails, matched by name, no scraper.![]() | ![]() |
| Overview — the four stages from source folder to console, each with the number that matters, and one recommendation for what to do next. | Every game — title screen, year, manufacturer, controls, screen, the folder it goes to, and whether its files match the release. |
![]() | ![]() |
| Selection — tick a genre, a category or one game; the counts and sizes are what that choice actually costs. | Transfer — what the next run will do, before it does it: 10,017 games left alone, 5 files renamed rather than copied again. |
![]() |
|
| Wanted — what the selection still lacks, priced from the release torrent, one button away from the download client. | On a phone — the same pages, laid out for a small screen. |
Marquee ships no ROMs, no disk images and no MAME data. It reads the machine list MAME itself publishes, the category files the MAME community maintains, and whatever your own download client has fetched. See Credits.
At a glance · Screenshots · Running it · The pages · Starting from nothing · Keeping a library current · What it does · How it works · Command line · Development · Credits · Licence
The image is on Docker Hub as biohazardious/marquee and on the GitHub Container
Registry as ghcr.io/biohazardious/marquee (amd64 and arm64, the same build and tags
on both), built by the CI workflow from every push to
master and every v* tag:
cp .env.example .env # point CONFIG, DOWNLOADS and LIBRARY at your folders
docker compose pull # or `docker compose build` to build from this checkout
docker compose up -d
docker compose logs # the first line has the URL, with the API key in it
Without compose:
docker run -d --name marquee -p 8585:8585 \
-v /path/to/config:/config -v /path/to/downloads:/downloads \
-v /path/to/library:/library -e PUID=$(id -u) -e PGID=$(id -g) \
biohazardious/marquee:latest
Tags: latest follows master; a release v0.4.0 is also 0.4.0 and 0.4; every
build carries its short commit as sha-abc1234.
TrueNAS SCALE (24.10 or later): Apps → Discover Apps → ⋮ → Install via YAML, and
paste deploy/truenas/custom-app.yaml with its three
host paths changed to your datasets. It runs as TrueNAS's own apps user (568), the
one its qBittorrent app uses, so both see the torrent folder the same way. A catalog
app for Discover Apps is in deploy/truenas/catalog,
tested with the catalog's own CI.
Unraid: save deploy/unraid/marquee.xml to
/boot/config/plugins/dockerMan/templates-user/my-marquee.xml, then Docker → Add
Container → Template: Marquee. The defaults follow Unraid's conventions (appdata,
PUID 99, PGID 100).
Anywhere else that runs containers -- Synology, QNAP, Portainer, a Raspberry Pi -- the compose file above is all it needs: one container, three folders, one port.
Open the URL it prints. The key is stored in the browser after the first visit, so
plain http://localhost:8585/ works from then on; it also lives in config/api_key
if you need it again. A server bound to localhost only asks for no key at all.
Where the key comes from, in order: the MARQUEE_API_KEY environment variable
if it is set (the thing to use on a NAS, where the container's log is the awkward
place to fish it out of); otherwise api_key in the config volume, generated on
the first start and kept across restarts. Change the variable and the old key stops
working; clear it and the file's key is back in force.
| Variable | Default | What it is |
|---|---|---|
PUID / PGID | 1000 | The user Marquee runs as. Use qBittorrent's, since they share the torrent folder. |
TZ | Etc/UTC | Timezone, for the log |
MARQUEE_API_KEY | (generated) | The key the web UI asks for |
MARQUEE_PORT (or PORT) | 8585 | The port inside the container. The health check follows it. |
MARQUEE_NO_UPDATE_CHECK | (unset) | Set to 1 to stop it asking GitHub for newer tags |
MARQUEE_INDEX_URL | (Pleasuredome's page) | Where the release listing is read from, for a mirror |
The running version and build (v0.5.0@7c17217, or local for an image built by
hand) are at the bottom of the sidebar and on the System page, which also says
whether a newer release has been tagged -- checked against GitHub twice a day, with
nothing downloaded.
Or without Docker:
pip install -e .
marquee --web # settings.ini beside the package
marquee --web --config ~/.config/marquee/ # or a config directory
--config takes either the settings file or the directory holding it, which is what
the container mounts at /config. Outside Docker the UI is on port 8777.
Requires Python 3.9 or newer. The only hard dependency is pysmb; paramiko is
needed for SFTP and is an optional extra.
A dark web UI with nine pages, in the order the work happens:
#library/mslug). Filter by condition, too: everything listed passes MAME's
working filter, but 3,229 of the 11,993 run with something imperfect about them and
each one says what. Download asks the client for exactly what the filters are
showing — it prices it first, and nothing is fetched until you say so.smb://192.168.1.20/Batocera3/roms/mame) it connects and reads the top level --
"Reached 192.168.1.20, share Batocera3: 48 entries (Music, Multiplay, …)" -- since
the folder picker can only walk this machine's own disks.Nothing downloaded, an empty library folder, a qBittorrent that has never seen a MAME torrent. That is the case the whole thing is shaped around.
Genre/Category/machine.zip, with a gamelist.xml and artwork for the
console.Where downloads go is qBittorrent's business, not Marquee's — it is configured there,
per category. What Marquee needs is to be able to read them: if the client reports
/data/torrents/... and this app sees the same files somewhere else, set a remote
path mapping in Settings. The download drawer says where the client is putting things
and warns when that path is not visible from here.
Three questions, three answers. This is the part a plain copy tool cannot do.
A name match says nothing. MAME rebuilds ROM sets between releases — a bad dump
replaced, a chip renamed, a mask ROM redumped — so strider2.zip from 0.252 and
strider2.zip from 0.289 are the same name and different bytes. A library that only
checks for existence carries those differences for ever, and that is the difference
between a copy of a romset and one that is kept up to date.
Check the library, on the System page or beside the Transfer summary, works on a local folder and on an SMB or SFTP share alike (each zip's central directory is read in place; plain FTP cannot seek, so it is the one destination that cannot be checked). It reads every zip's central directory — entry names and CRC-32s, no unpacking — and compares them with the release's own ROM list, one ROM at a time. About 9 ms a game on a local disk (a 10,000-game library in under two minutes), and about five minutes for the same library on a share over the LAN.
Each game comes back as current, out of date (a ROM is there with the wrong contents, or one it owns outright is missing), missing an inherited ROM (what a merged or split set looks like from here, and not evidence of the wrong version), damaged, or absent. Anything out of date joins the download list, with the reason on the row:
NBA Showtime NBA on NBC 2.7.u27: 4242bf14 not 44a086a1
NFL Blitz 2000 Gold Edition 494_blitz_2000.u96: missing
Total Vice (ver EBA) 93c46.7k: 25aa0bd1 not 9c34554a
A real 0.252-era library of 9,904 games measured against 0.289: 9,350 current, 386 out of date, 168 missing an inherited ROM — 5.8 GB to bring up to date, against 250 GB to fetch the set again. What the check finds outranks what the file sizes say, so a redump that happens to weigh the same as the dump it replaces is still replaced.
The Transfer page is the diff between the library and the selection, in five rows, each of which opens into the games and files it means:
| Copy over | not at the destination at all |
| Replace | there, but not what this release says it is |
| Move | the same file, in a folder this release no longer uses — renamed, never re-copied |
| Delete | at the destination, wanted by nothing — left alone unless you ask |
| Leave alone | already correct, and not touched |
Each kind can be read as one list or by genre -- the same genre → category → game tree the Selection page uses, so a run reads as what it does to the library ("Platform: 65 games, 38 GB, most of it Run Jump") and not as 1,144 lines.
Nothing is deleted unless the box is ticked, and every candidate says why it is one: you left it out, not in this release, a disk this release does not list for it. "Delete 917 files" is not something anyone can agree to; that list is.
The Move row is what makes an upgrade cheap. A machine's path changes between
releases as well as its contents — catlist renamed Casino to Gambling in 0.289, and
a clone with a disk of its own moves out of its parent's folder — and those are renames,
not downloads. On a real 0.252-era library moving to 0.289 that was 954 files and
23.7 GB relocated rather than deleted and fetched again, and 939 games that stopped
counting as missing.
Point Library at an existing romset and build a plan. What is already there and still wanted is counted as already there — not copied again, and not reported as something to download. A real 0.252 library came out as 7,256 of 8,764 games already present, 1,508 to fetch and the rest accounted for.
The source folder does not have to hold anything for this: the library is a statement about what you have, not only a place to put things.
mameXXXX.xml, read the ROM data, and filter out non-working drivers,
prototypes, betas, screenless devices, mechanical cabinets and BIOS setsGenre/Category/Two layouts: every game under Genre/Category/, or every game in one folder —
what MAME itself, RetroArch and most frontends read, since none of them look inside
subfolders. The adult and console folders stay folders either way. Switching is a
transfer of renames: on a test library, 85 games went to one folder and back with
nothing copied, and the gamelist followed.
Write a gamelist.xml at the root of the library, so EmulationStation shows
Metal Slug - Super Vehicle-001, 1996, Nazca, 2 players instead of mslug — every
field of it is already known, nothing is scraped
Place the downloaded artwork beside the games
One game, one ROM: keep each family's parent and leave its other revisions out. On MAME 0.289 that is 7,538 fewer machines and 80 GB less — Dragon's Lair alone ships four revisions at 11.5 GB each. A game whose parent did not survive the filters keeps one version, so nothing disappears.
Preferred regions: pick World, USA, Europe (or any order you like) and a version from anywhere else is left out when the game has one from those; a game released only in Japan stays. MAME's own tags decide it — "(US, set 1)", "(German)", "(Export)" — and a country counts as its continent. On 0.289 that sets aside 1,582 versions and loses no game; 97% of the Japanese ones it finds were already on one user's hand-typed exclude list. With one game, one ROM, the first region that has a proper release is the version kept — never a bootleg over the real thing.
What the cabinet can play: switch on the controls it has — joystick, twin sticks, wheel and pedals, spinner, trackball, light gun, mahjong and casino panel — the buttons per player and the screen's orientation. A game needing anything else goes to Left out, with the reason. "Gamepad" and "Arcade stick" presets included.
A rating on the console: AntoPISA's bestgames.ini score goes into
gamelist.xml as stars, only where the game has none — a rating you or a scraper
gave is kept. The Library sorts by it too.
The console's own MAME: Batocera ships its own, often a few releases behind the
set, and older full sets are not published. Name it in Settings and every game is
held to that release's XML: machines it does not have go under
ZZ-Version-Mismatch/, games whose zip lacks a ROM it still asks for under
ZZ-Missing-ROM/, genre and category kept inside. On a 0.289 library under
Batocera's 0.285 that was 714 and 67 of 10,022 — the other 9,241 run as they are,
since MAME finds a ROM by CRC. Nothing is copied or deleted: they move by rename,
and move back when the console catches up.
An ignore list for what is in the library but not Marquee's — a BIOS pack's
neogeo.zip, pgm.zip and friends at the top level, for the older libretro cores.
Never deleted, moved or counted. *.zip covers every zip at the top level and
nothing below it; the Transfer page's delete list adds files with one click.
Content type
Image
Digest
sha256:1ad125110…
Size
44.6 MB
Last updated
1 day ago
docker pull biohazardious/marquee