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.
- Choose by genre, category or single game. One tree, shaped like the folders on your console, with the game count and the gigabytes each choice costs.
- Fetch only what you chose. Marquee narrows the Pleasuredome torrents in qBittorrent to the files you picked, prices it before anything starts, and never deselects a file you are already seeding.
- Upgrade a release at a time. It compares two MAME catalogues and fetches only the machines whose bytes changed.
- Prove what is on the console. Every zip is read against the release's CRCs, and every disk can be hashed against the torrent's own pieces — a copy that stopped half-way is found, not assumed.
- Write straight to the console. A local folder, SMB, FTP or SFTP; a
gamelist.xmlthat keeps EmulationStation's favourites and play counts; title screens from libretro-thumbnails, matched by name, no scraper. - Run it anywhere. One Docker image for amd64 and arm64, no database, a web UI that works from a phone. See Running it.
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 itWithout 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:latestTags: 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:
- Overview — where the library stands and what to do next. The four stages (source folder → selection → fetch → library) as cards with the number that matters on each, one recommendation at the top worked out from the same figures ("2.9 GB ready to go into the library", "your settings moved on — rebuild the plan"), the health of the setup, the free space the next transfer needs, and the last runs. It is the page the app opens on. The topbar pill says the same thing from every other page, and the status bar under it says what is happening right now -- "Transferring 1.1 GB of 2.9 GB · 118 MB/s · 15s left · mslug.zip", "Checking the library 4,210 of 10,223", "Stopped: no space left on device" -- with the download client beside it: unreachable and why, connected, or "Downloading 1 release · 11.9 MB/s · 38 GB left · 55m left".
- Library — every machine the selection covers, downloaded or not, as a poster
grid or a dense table. On a fresh install that is the whole catalogue: nothing is on
disk, and the point of the page is choosing what should be.
Title screens come from libretro-thumbnails, matched by MAME's own description, so
there is no scraper and no lookup table; Artwork downloads them all so the
page loads from disk and works offline, and the grid redraws itself when it
finishes. Filter by genre, by sync state, or by whether a title is adult. Click a
game for year, manufacturer, players, controls, screen type and resolution, where it
will be filed, its files and its other revisions. Each game has its own link
(
#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. - Selection — one tree shaped like the library on disk: genre → category → game, and then a branch named after your adult folder holding the same genres and categories underneath it. catlist marks adult categories in the category name, so the two halves are genuinely different categories filed in different folders, and each side ticks without disturbing the other. Every row is a tick box with its game count and size. Untick a genre, a category, or a single game; tick it again to put it back, then Save selection. The tick box decides; clicking the game's name opens it. The counts are what the selection asks for, not what happens to be downloaded, so the tree means something on a fresh install. It takes the library's filters too — genre, condition, adult, downloaded or not — and when one is set the tree becomes a flat list of what matched, with Leave all out / Put all back acting on the whole match rather than the page of it. Dropping every imperfectly emulated game is 11,993 → 8,764 games and 494.9 → 322.6 GB. Only the largest 500 games of a category are drawn, but every tick box covers all of them. Unticking something that is already on the console is a deletion waiting to happen, so the heading says so at once -- "· 312 in the library · 4.1 GB to delete" -- rather than only under Delete on the Transfer page after a rebuild. Nothing goes until that page's "Also delete" box is ticked.
- Wanted — the games the selection asks for that are not on disk yet, grouped by genre and priced from the release torrent, with a button that asks the download client for exactly those. Also where you move the library to a newer MAME release: it compares the two catalogues and fetches only the machines whose bytes actually changed. A partly-downloaded set is the normal state of things, so this gets a page rather than a wall of names in a log.
- Left out — everything that is not in the library, in the same tree as the library. Six reasons: five are the working filters (a driver that does not run, a BIOS set, a prototype) and are there so the question has an answer; the sixth is the games you excluded by name, and those have a Put back on them. Excluding a game used to be a one-way door — it was dropped before anything else ran, so it appeared in no list, no search and no tree.
- Transfer — what the next run will do, before it does it: what is copied, what is replaced, what is merely renamed, what would be deleted and what is left alone. Every row opens into the games and files it means. See What a transfer will do.
- Activity — the running job, the download queue, and the log.
- Settings — media management, download client (with a Test button), indexer.
Only the ROM folder and the library are required: the CHD folder defaults to the
ROM folder (the disks are found inside it), and the MAME release to whatever the
source folder's name says.
The Library field has a Test button: for a share on the console
(
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. - System — which XML and catlist are in use, what the library on disk records, Check the library, and the breakdown of every machine the filters dropped. On 0.289 that is 4,323 of 16,350 before your own exclusions — 2,861 with no screen at all, 949 whose driver does not work, 306 prototypes and betas, 206 BIOS and device sets.
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.
- Settings → point Library at where the games should end up, fill in the Download client, press Test. Leave the ROM and CHD folders at the folder your client downloads into; Marquee finds the set inside it.
- Build plan. No files are needed for this — it reads MAME's own XML and catlist.ini for the release you chose and works out the catalogue. About 12,000 games, filed by genre and category.
- Library → filter. By genre, by search, by adult, by year — whatever narrows it to what you actually want.
- ⬇ Download. It reads the release's file table — which means adding the torrent to your client stopped, so its own metadata arrives and none of its content — and tells you what that selection costs. 16 Metal Slug games: 936 MB of a 152 GB set. Confirm, and the client fetches those files and skips the other 44,150.
- When it finishes, Build plan again, look at Transfer, and run it. The games
land under
Genre/Category/machine.zip, with agamelist.xmland 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.
- Parse
mameXXXX.xml, read the ROM data, and filter out non-working drivers, prototypes, betas, screenless devices, mechanical cabinets and BIOS sets - Find the MAME XML and catlist.ini by itself, and refuse to run when the two are for different MAME versions
- Derive genres and file every machine under
Genre/Category/ - Separate ROM and CHD source folders, and a separate destination folder for adult titles
- List the missing ROM and CHD files, so only what is needed is downloaded
- Download the matching catlist.ini by itself when it is not already on disk
- Exclude by ROM name, by genre or by category
- Dry run, a written report of what is missing, and a free-space check before copying
- Copy to a local folder, or to a console over SMB, FTP, FTPS or SFTP
- Sync rather than copy: work out what is new, changed, merely moved, or no longer wanted
- Relocate a recategorised machine instead of re-copying it
- Optionally remove destination files the plan no longer wants, for romset upgrades
-
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.xmlat the root of the library, so EmulationStation shows Metal Slug - Super Vehicle-001, 1996, Nazca, 2 players instead ofmslug— 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.iniscore goes intogamelist.xmlas 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 underZZ-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.zipand friends at the top level, for the older libretro cores. Never deleted, moved or counted.*.zipcovers every zip at the top level and nothing below it; the Transfer page's delete list adds files with one click.
- Read the Pleasuredome index and list every published set, its version and its magnet
- Drive qBittorrent over its Web API: add a release, select exactly the files your filters want, and leave the other 32,000 at priority 0
- Cost a download before starting it, to the byte — including the pieces BitTorrent makes you fetch for files you did not ask for
- Work out a version upgrade from content signatures, so seven releases cost the same as one and only the machines you keep are considered
- Hardlink the categorised library out of the torrent folder when they share a filesystem, so the library costs no extra space and seeding carries on
- Rewrite the download client's view of a path into yours, for when it runs in another container or on another host — qBittorrent's side picked from the folders it already uses, yours with the folder browser
- Say how long a fetch will take, at the pace Marquee's own downloads have run
- Give the torrent folder's space back once the library has the files (Activity → Reclaim), with each torrent's ratio and seeding time in view; files that no plan uses any more are named, and go only with a second, separate confirmation
- A console that is off is not an empty library: the plan stops and says so, and Marquee knocks on the share once a minute and plans by itself when it answers
qBittorrent allocates every selected file at its full size before a byte of it arrives, so a torrent folder half-way through a download holds thousands of zips that are the right size and nothing but zeros. Marquee reads the last 128 bytes of every zip (the end-of-archive record has to be there) and the first 8 of every CHD (its header) before calling a file downloaded -- a fraction of a second for 14,000 files. Anything that fails is "downloading", not "on disk", and is never copied.
- Fix broken ROMs or rebuild sets — use a ROM manager such as clrmamepro or RomVault
- Scrape box art, videos or metadata — use Skraper or EmulationStation's own scraper
- Include, host, seed or link to any ROM, CHD or BIOS file. It works with the sets you already have, or that your own download client fetches
- Handle Software List sets. They are indexed so you can see them, but the library is arcade only
A run does not copy blindly. It indexes the destination first and works out, per file, which of the five things above to do with it. A file that is not where it belongs but is somewhere else at the destination under the same name is a rename, so a recategorised machine costs a directory operation rather than its own size — on a real 0.282 set, two renamed folders holding 79 files and 12 GB resynced in 0.3 seconds with nothing written, against about forty seconds of disk traffic for a plain copy.
--no-compare skips the destination index and copies the old way. Over SMB the index
costs one directory listing per folder rather than one per file.
Disks are located by name, not by folder. MAME searches a clone's parent set as well as
the clone itself, so CHD collections routinely keep a clone's own disk inside the
parent's folder — zokuoten/zokuotena.chd rather than zokuotena/zokuotena.chd.
Looking for a folder named after the machine therefore reports those clones as missing
when the file is right there; on a 0.282 arcade set that was 101 of 101
reported-missing CHDs.
At the destination each machine gets the disks it actually needs:
- a clone whose
<disk>has amergeattribute shares the parent's file, so it is placed under the parent's name and MAME's parent lookup finds it — copying a multi-gigabyte disk twice is the only alternative; - a clone without
mergehas a disk of its own and gets its own folder.
Only the named disks are copied, so a source folder holding several clones' disks no longer drags all of them along.
Only catlist.ini is needed. The other two files this project used to ask for carry no information it cannot work out on its own:
genre.iniis the leading component of every catlist.ini section (Arcade: Maze / Misc.→Maze). Checked against MAME Extras 0.252 (48 genres, 45,379 machines) and 0.289 (59 genres, 50,368 machines) — both match exactly, so it is derived instead of read. Note the separator is" / "and not a bare slash: a genre can contain one of its own, as inGame Console/ComputerorVideocassette Player/Recorder.screenless.inilists the machines the XML gives no<display>element, so it is derived from the XML itself. This also means it can never be out of step with your romset. Pointscreenless_iniat an old copy and Marquee will compare the two and report any difference.category.iniwas never read at all.
catlist.ini and mameXXXX.xml are looked for next to the package, in ./MameFiles/,
beside each other, and in the usual MAME locations (~/.mame, /usr/share/mame,
Batocera, RetroPie, Recalbox). If no XML turns up and mame is on PATH, mame -listxml is used to produce one. Both carry a version stamp — ;; CATLIST.ini 0.252 / ... / MAME 0.252 ;; and the XML's build attribute — which are compared before
anything else happens.
A catlist.ini for another release is skipped rather than used: it is found first,
files machines under categories they have since moved out of, and the run then stops
on a mismatch over a file nobody remembered putting there. Marquee says which one it
ignored and fetches the matching one instead. Only --offline, with nothing else to
use, falls back to it — and says so.
Tell it which MAME release your romset is and both the machine list and the categories are fetched for exactly that release — nothing to download by hand, nothing to keep in step. The MAME release field on the Settings page lists every version that has both; left on Automatic, the release is read off the source folder's name, which is how Pleasuredome names its sets ("MAME 0.289 ROMs (non-merged)" means 0.289), so a fresh install with the torrent folder pointed at needs nothing chosen at all.
marquee --list-versions
marquee --mame-version 0.289 --web
Support files come from AntoPISA/MAME_SupportFiles, whose commits are tagged with the MAME version they carry, and the machine list from AntoPISA/MAME_Dats. The downloaded file's own header is checked against the version that was asked for, so a reorganised archive cannot quietly hand over another romset's categories.
The two repositories do not move together, so a version can be categorisable but not
parsable: at the time of writing 17 of 28 published versions have both, and 0.288
is one of those that does not. --list-versions says which and why rather than hiding
them, and any version still works if you supply the XML yourself or generate it with
mame -listxml. That history reaches back to MAME 0.262; for anything older, take
the MAME Extras pack for your version from progetto-SNAPS and drop
catlist.ini in ./MameFiles/. --offline turns the network off entirely.
The XML parse is cached under ~/.cache/marquee (inside the container, /config/cache),
keyed on the file's size and timestamp, so only the first run pays for it.
After every transfer a small .marquee.json is written into the destination root:
{ "mame_version": "0.282",
"settings": { "blacklist_genres": [], "blacklist_categories": [] },
"stats": { "machines": 9914, "bytes": 280513 },
"history": [ { "date": "...", "from": "0.279", "to": "0.282", "copied": 412, "moved": 79 } ] }It is what lets the app open and say this console holds MAME 0.282, 9,914 machines, synced on Tuesday instead of asking, and it tells a fresh install apart from an upgrade. The filters travel with the set, so pointing at a destination can load back the selection it was built with — handy after a reinstall, or to copy one console's setup to another.
Only .zip and .chd files are ever considered this tool's business. A destination is
somebody's console folder: scraped artwork, gamelist.xml, ES metadata and the record
itself are never counted as leftovers and never removed.
Everything the web UI does can be done without it.
marquee [--config settings.ini] [--setup] [--web [--host H] [--port N] [--open]]
[--rom-dir DIR] [--chd-dir DIR] [--dest DIR|smb://...]
[--mame-version X.XXX] [--list-versions]
[--xml mame0252.xml] [--catlist catlist.ini] [--mame-binary mame]
[--fetch-support-files] [--offline] [--ignore-version-mismatch]
[--no-cache] [--refresh-cache]
[-n|--dry-run] [-y|--yes] [-q|--quiet] [--report plan.json]
[--delete-orphans] [--no-compare] [--allow-mature|--no-mature]
--setup asks for the three paths that cannot be guessed and writes a settings.ini for
you; running with nothing configured offers the same thing. The paths can also be given
entirely on the command line, in which case no settings.ini is needed at all:
marquee --rom-dir ~/mame/roms --chd-dir ~/mame/chds --dest /media/batocera/roms/mame -n
--dry-run reports the plan — how many machines, how many files, how much space, what
is missing, and what would actually be transferred against what is already at the
destination — and stops. --report writes the same thing to a .json or .csv file,
so the missing-ROM list can be fed to whatever you download with. Before copying
starts, the space the run needs is compared against what is free at the destination.
By default settings.ini is read from the folder the package lives in, and relative
paths inside it are resolved against that same folder, so it can be run from anywhere.
MameCleaner.py in the checkout is the same entry point under its old name and takes
the same flags.
Use one MAME version for everything: a 0.251 romset wants mame0251.xml, the 0.251
catlist.ini and the 0.251 CHDs. Marquee checks this and stops if they disagree.
Non-merged ROM sets are strongly recommended — each zip is then self-contained,
which is what makes per-game picking work.
settings.ini holds the paths, the selection and the download client. Copy
settings.ini.example and edit it, run marquee --setup, or
just use the Settings page — it writes the same file. In Docker it lives in the
CONFIG volume, beside the API key and the caches.
A Docker bind mount resolves to an inode when the container starts, not to a path. So renaming the library folder on the host — or deleting and recreating it — leaves the container looking at the old directory, with its old contents, for as long as it runs. The host shows an empty folder, the app shows 28 genres, and neither is lying.
docker compose up -d --force-recreate # re-resolves every mountIt is worth knowing which way round it went: if the old folder was renamed rather than deleted, the files are still there under the new name, and the mount is what was keeping them reachable.
A torrent client fetches the first and last pieces of a file early, so a 14 GB disk at 0.4% already carries its CHD header and a zip its end-of-archive record. Marquee used to take either as proof the file was finished and copied it into the library as zeros of exactly the right size, which the size-based comparison would never question again.
Since 0.5.1 a plan asks the download client, file by file, what is still arriving (a torrent reads 100% once its unwanted files are deselected, so a disk deselected at 96% is exactly the one to ask about); those wait, marked partial, until the next plan. Without a client, the body of each source file is sampled for runs of zeros.
If a transfer ran against a half-fetched set before this, run Check the library. It opens every disk as well as every zip. A zero-filled sample is held against the finished copy in the download folder — the same bytes there mean the disk is genuinely like that, since real CHDs carry runs of zeros too — and a disk that differs is damaged, which the next transfer replaces. Deep check goes further: it reads every disk in full and hashes it against the torrent's own piece hashes, which is the one exact answer, at the cost of reading the whole library once.
The selection wants CHDs and none have been fetched. Before 0.5.1 the Wanted page only priced zips, so with every zip accounted for it said nothing to fetch while hundreds of disks were missing, and the library sat a hundred gigabytes short. The Overview's Health panel now says so directly: CHD folder — no CHD set under /downloads yet — N wanted disks are neither there nor in the library, and What would it cost? on the Wanted page quotes the disks from the CHD set alongside the zips from the ROM set. Disks already in the library are not fetched again.
Marquee reads the ROM and CHD sets from the folders in Settings, or from a torrent
folder one level below them — never deeper. A backup of the full set kept further
down (/downloads/mame/…) is left alone on purpose: a plan that read from it would tie
the library to a folder Marquee does not manage. Point the CHD folder at such a set
explicitly if that is what you want.
First check ownership. Marquee and qBittorrent write into the same torrent folder,
so they should run as the same user: give Marquee the PUID/PGID qBittorrent uses
(on TrueNAS apps that is often 568). qBittorrent's own log (Tools → Log) says
"Permission denied" when this is the problem, and a chown of the torrent dataset's
root back to qBittorrent's user fixes an instance that has already been bitten.
Versions before 0.5.0 took ownership of /downloads themselves at start-up; this
one does not touch it.
Downloads land where qBittorrent puts them -- its default folder, or the
marquee category's folder -- and Marquee never changes that. An "error" state the
moment a torrent starts is nearly always that folder not existing, or not being
writable, inside qBittorrent's own container. Fix it there (a folder it can write
to, or the category's save path in its settings); Marquee's Download folder setting
has nothing to do with it.
What Marquee's settings decide is only how it finds those files afterwards, and
normally that takes one setting: the Download folder, the torrent folder as this
container sees it. Each torrent's folder is looked for by name inside it, so
qBittorrent may call the same folder /data/torrents/MAME 0.289 ROMs (non-merged)
and Marquee /downloads/MAME 0.289 ROMs (non-merged) and nothing needs writing
down. A remote path mapping (qBittorrent's path -> this app's path, one per
line) is only for when that lookup cannot work. The Test button beside the client
settings says where a new download would land, how Marquee sees it and whether it
can; if an earlier Marquee left the marquee category pinned to a folder qBittorrent
cannot use, the same result offers a one-click reset to qBittorrent's default folder.
The client is reached from wherever Marquee runs. In Docker that is the container, so
localhost means the container itself — use the host's address, or put both on the
same Docker network. Test in Settings reports what went wrong, and on success says
which qBittorrent answered and where it is downloading to; if that folder is not one
this app can read, set a remote path mapping beside it.
marquee/
config.py Config dataclass, settings.ini reading and writing
sources.py finding MAME data, version headers, streaming parse, parse cache
fetch.py downloading version-matched support files
catalog.py filtering, screenless/genre derivation, categories, folder names
plan.py deciding what to copy, free space, reports
sync.py diffing a plan against what is already at the destination
verify.py reading what a zip actually holds and checking it against the release
art.py title screens from libretro-thumbnails, cached locally
gamelist.py the file EmulationStation reads
manifest.py .marquee.json: what the destination remembers
pipeline.py build_plan() and execute(), the whole job with no terminal attached
acquire.py mapping wanted machines onto file indices inside a torrent
acquisition.py what a download would cost, and driving a client to make it
indexers/ the Pleasuredome index and release datfiles
download/ the qBittorrent Web API client
backends/ local, SMB, FTP and SFTP destinations
reporting.py Reporter protocol: terminal, collecting, silent
errors.py MarqueeError and its subclasses
cli.py flags, prompts, printing, exit codes
web/ server.py, application.py (+ settings, downloads, releases, tasks),
job.py, views.py and a single-page front end
Nothing below cli.py prints, prompts or calls sys.exit; it reports through a
Reporter and raises MarqueeError. Planning and copying are separate calls, so a
caller can show the plan and decide before anything is written:
from marquee import Config, pipeline
config = Config(rom_dir="~/mame/roms", chd_dir="~/mame/chds",
copy_path="/media/batocera/roms/mame")
plan, resolution = pipeline.build_plan(config) # copies nothing
print(len(plan.items), plan.missing_roms)
summary = pipeline.execute(plan, config) # only if you decide toPass a Reporter subclass to either call to receive progress and messages instead of
having them printed.
CI runs the tests on Python 3.9 and 3.13 for every push and pull request, and pushes
the image for master and tags. Two repository secrets make the push possible:
DOCKERHUB_USERNAME and DOCKERHUB_TOKEN (an access token from Docker Hub's
Account settings → Security). To publish a version: bump version in
pyproject.toml, then
git tag v0.4.0 && git push origin v0.4.0pip install -e ".[dev]"
pytest
The suite runs against a small synthetic romset in tests/fixtures/, so it needs no
MAME data of its own. Where it makes a claim about the real MAME Extras files — that
genre.ini is redundant, for instance — it checks that against MameFiles/ when you
have put the real thing there, and skips it otherwise. The front end has no build step
and no framework; its modules are checked by the suite too.
Tested on Linux and Batocera; it should work on RecalBox, RetroPie and other distros.
See docs/ARCHITECTURE.md for the design and the measurements behind it, and docs/ROADMAP.md for what was built, what was measured and rejected, and what is deliberately not being done.
Marquee is a thin layer over other people's work. It bundles none of it and fetches what it needs on demand.
- MAME and MAMEdev — the emulator and, through
-listxml, the machine data everything here is derived from: ROM and disk lists, CRCs, driver status, displays, inputs and parent/clone relationships. MAME is licensed GPL-2.0-or-later. - AntoPISA / progetto-SNAPS —
catlist.ini, the category file that gives every machine its genre and category, and so the entire folder layout. Fetched version-matched from MAME_SupportFiles; the machine list comes from MAME_Dats. Neither is bundled here. - Pleasuredome — the MAME torrents and their datfiles. Marquee reads the public index to learn what has been published and hands the magnet to your own client; it hosts nothing and seeds nothing.
- libretro-thumbnails — title screens, snapshots, box art and logos,
named after MAME's own
<description>, which is why no scraper is needed. - qBittorrent — the download client driven over its Web API v2.
- EmulationStation and the Batocera, RetroPie and
Recalbox projects — the
gamelist.xmlformat and the folder conventions the library is written for. - Sonarr and Radarr — the shape of the thing: a library you declare, a client that fetches it, and a page that tells you what is missing.
- pysmb and paramiko for the SMB and SFTP destinations.
GNU General Public License v3.0 or later. See LICENSE.
Marquee — a MAME library manager
Copyright (C) 2026 Baybars HAZAR (Biohazardious) and contributors
This program is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
GPLv3 is the same licence Sonarr, Radarr and the rest of that family use, which felt like the right company to keep.
The licence covers Marquee's own code. MAME, the support files, the thumbnails and anything you download with it belong to their respective authors and carry their own terms. You are responsible for having the right to the ROMs you point this at.







