gamarr finds great games and puts them in your library — no browsing, no manual searches, no cleanup. A configurable pipeline scrapes Metacritic for critically-acclaimed releases, filters out unwanted genres and keywords, matches titles against FitGirl repacks and FreeGOG, and delivers torrents to qBittorrent. Once downloads complete, a post-processing thread SHA-256-verifies and copies each game to your library with customizable path templates, then cleans up source torrents. Runs as a daemon so your library stays fresh without lifting a finger.
- Two-phase score verification — browse pages use internal Metacritic metrics (not real 0–100 scores) to build a candidate pool quickly. Browse scores are on a completely different scale (e.g. 1478, 1931) and always pass the configured thresholds — the real 0–100 score filtering only happens during the detail-page verification step. This avoids hundreds of slow HTTP requests while keeping score accuracy.
- Genre-based rejection — exclude games by genre using case-insensitive
substring matching (e.g.
["RPG"]rejects "Action RPG", "JRPG", etc.). - Keyword-based filtering — reject games whose titles or FitGirl page description contain specific keywords.
- Date-based backlog cutoff — set the oldest month to review with
max_date: 202401and how much of the backlog one cycle covers withmax_cycle_months: 2. Pages remain available asmax_pages/max_cycle_pagesforcriticscore/userscore, whose browse lists are score-ordered and have no date dimension. - Verify before queueing — a game's real Metacritic detail-page scores are
checked before it enters the pending queue, so the queue only ever holds
games that already pass the thresholds. Games that fail, or whose scores are
not published yet, are not queued: they are re-checked when a browse pass next
reaches their pages (immediately in
latestmode, when the sweep wraps inbacklogmode) instead of occupying the queue for days. - Source-matching expiry — a queued (already verified) game waits
download_sites.fitgirl.max_queue_daysfor a matching repack (default 60, or0for indefinite; the FitGirl entry supplies the window for every queued game). Every game is queued already verified, so it takes that window at insert, and later re-checks do not push the window forward, so a verified game with no match still leaves the queue when the window lapses. Without a FitGirl source, the source-matching window falls back to the Metacritic-sidemax_queue_days. - Multiple download sources — supports FreeGOGPCGames (A-Z list-based) and FitGirl repacks (sitemap-based). Sources are configured as an ordered list — position determines priority, and the first source with a match delivers the torrent.
- Source priority — if a game is available on multiple sources, the highest-priority source in the config list is used. Games that don't match any source remain in the pending queue.
- Database deduplication — every processed game is recorded in SQLite; previously processed titles are never re-processed.
- Configurable cache TTLs — Metacritic scores and browse pages are cached with independent TTLs to reduce HTTP requests.
- Notifications — sends alerts via any apprise-compatible service (ntfy, Discord, Telegram, email, and more).
- Schedule mode — runs as a scheduled background process with separate intervals for acquisition and post-processing, or in foreground mode for single-pass execution.
- Automatic config migration — upgrades the YAML config schema automatically on startup when new fields are added.
- One-time queue cleanup — on first start after upgrading, pending games that never passed
detail verification are deleted. They are re-discovered when a browse pass next reaches their
pages (immediately in
latestmode, when the sweep wraps inbacklogmode). Verified games awaiting a source match are kept. - Post-processing — a background thread polls qBittorrent for completed
downloads, copies them to a configurable library path with SHA-256
verification, and cleans up source torrents after seeding goals are met.
Torrents whose metadata has not been fetched yet (a paused magnet holds no
files) are not complete: they are skipped quietly and reported once per cycle
in the summary line (
N downloading, M awaiting metadata), never as a copy failure.
- Python 3.12+
- Astral uv
- qBittorrent with WebUI enabled
git clone https://github.com/binhex/gamarr
cd gamarr
uv venv --quiet
uv sync# Validate configuration
gamarr --test
# Run a single acquisition cycle
gamarr| Option | Description |
|---|---|
--config-path <dir> |
Directory containing gamarr.yml (default: configs) |
--log-level <level> |
Override console log level (DEBUG, INFO, SUCCESS, WARNING, ERROR) |
--log-path <dir> |
Override the log file directory from config (logs are written to <dir>/gamarr.log). |
--db-path <dir> |
Override the database directory from config |
--pid-path <dir> |
Override the PID file directory from config |
--library-path <paths> |
Override library paths (pipe-separated: --library-path /a|/b) |
--qbt-host <host> |
Override qBittorrent host from config |
--qbt-port <port> |
Override qBittorrent WebUI port from config |
--qbt-username <user> |
Override qBittorrent username from config |
--qbt-password <pass> |
Override qBittorrent password from config |
--clear-cache <sources> |
Clear cached data. Comma-separated: freegog, fitgirl, metacritic, or all |
--test |
Validate configuration and exit |
--version |
Show version and exit |
--help |
Show help message and exit |
All behaviour is controlled by a YAML file inside the config directory
(configs/gamarr.yml by default). A default config is created automatically
on first run. The file is divided into the sections below.
| Key | Description | Default |
|---|---|---|
config_version |
Schema version — managed automatically; do not edit. | (current) |
daemon_mode |
foreground or background. Scheduling is controlled by schedule.enabled. This field is deprecated. |
foreground |
log_level_console |
Console logging level. | INFO |
log_level_file |
File logging level. | INFO |
log_path |
Directory for log files (gamarr.log is created inside). |
logs |
db_path |
Directory for the SQLite history database (gamarr.db is created inside). |
db |
pid_path |
Directory for the PID file (gamarr.pid is created inside). |
pids |
library_path_list |
Override library paths from CLI (--library-path). Mapped to library.paths at runtime. |
[] |
| Key | Description | Default |
|---|---|---|
enabled |
Enable or disable scheduled background mode. | false |
schedule_time_mins |
Interval in minutes between scheduled cycles. | 60 |
run_on_start |
Run immediately on start, before the first interval. | true |
acquisition_timeout_mins |
Hard watchdog budget (minutes) for one acquisition cycle. A cycle that exceeds this is aborted so the next scheduled run can proceed. Bump for very large fresh source indexes. | 25 |
download_sites is an ordered list of source configurations. Each
entry is a single-key dict where the key IS the source name.
Position in the list determines priority — the first source that has
a match delivers the torrent.
download_sites:
- freegog:
enabled: true
platform: pc
cache_hours: 6
reject_keywords: []
max_queue_days: 60
- fitgirl:
enabled: true
platform: pc
cache_hours: 6
reject_keywords: []
max_queue_days: 60| Field | Description | Default |
|---|---|---|
enabled |
Enable or disable this source. | true |
platform |
Target platform for matching. | pc |
cache_hours |
How long to cache the source index before re-fetching. | 6 |
reject_keywords |
Reject games whose FitGirl page title or "Repack Features" block contains any of these keywords (case-insensitive). Matching is whole-word and negation-aware against the page title and the "Repack Features" block only, so "non-HV repack" does not match hv. |
[] |
max_queue_days |
How many days a game stays in the pending queue after its scores are verified (the source-matching phase). 0 = indefinite pending (no expiry). |
60 |
| Key | Description | Default |
|---|---|---|
min_criticscore |
Minimum Metacritic critic score (0–100). | 75 |
min_criticscore_reviews |
Minimum number of critic reviews required. | 10 |
min_user_score |
Minimum Metacritic user score (0–10). | 7.5 |
min_user_reviews |
Minimum number of user reviews required. | 10 |
mode |
Discovery mode. "latest" reads up to max_pages pages of the window every cycle (no cursors, no pacing) so new releases are always seen, spending the depth newest year first and keeping one page for the immediately lower year; "backlog" sweeps back through the window with per-year cursors and pacing. Latest requires sort_order: "new" — a score sort is warned about and coerced to "new" for that cycle. |
backlog |
max_pages |
Page budget. In backlog mode: total browse page depth budget; the scanner advances across cycles and resets to page 1 when exhausted. In latest mode: depth — the first N pages of the window are read every cycle. 0 = no page budget in backlog (the sweep is still bounded by the 10-year year window) and is clamped to a depth of 10 in latest. |
500 |
max_cycle_pages |
Maximum pages per cycle (pacing). 0 = unlimited. |
0 |
max_date |
Date floor for the new sort order: sweep back to and including this month (YYYYMM with month 01-12 and year 1958 or later, e.g. 202401). 0 = disabled. It bounds the window and is ignored for criticscore/userscore; in backlog mode it replaces the max_pages budget, while in latest mode max_pages still sets the depth inside that window. A month later than the current one is clamped to the current month. |
0 |
max_cycle_months |
Pace in date mode: how far back one cycle may sweep, counted in months from the cycle's newest release month (a month is included when a page holds any game at or after its first day, so a cycle can cover N+1 calendar months at a boundary). The floor set by max_date always wins — this value can only shorten a cycle, never extend past it. 0 = no month pacing; the maximum is 1200 (100 years), and a larger value is rejected at config load. |
0 |
sort_order |
Discovery and pending processing order: "new" (newest release date first), "criticscore" (verified games by highest critic score), or "userscore" (verified games by highest user score). Queued games are already verified (see "Verify before queueing"). Changing this value reprioritizes the entire pending queue, including games from previous cycles. |
"new" |
max_queue_days |
Fallback expiry window for queued games, used only when no FitGirl source is configured (its max_queue_days normally governs the source-matching window). 0 = indefinite pending (no expiry). |
30 |
enabled |
Enable or disable the Metacritic browse step. Disabling skips game discovery entirely. | true |
cache_details_days |
Days to cache Metacritic detail-page results. | 7 |
cache_hours |
Hours to cache Metacritic browse-page results. | 6 |
reject_genre |
Reject games whose Metacritic genre contains any of these substrings (case-insensitive). E.g. ["RPG"] matches "Action RPG", "JRPG", "Western RPG". |
[] |
reject_title |
Reject games whose title contains any of these substrings (case-insensitive). E.g. ["Remake"] matches "Resident Evil 4 Remake", "Remake Collection". |
[] |
Games are verified against their Metacritic detail page before they enter the pending queue, so the queue only ever holds games that already pass the score thresholds. Games whose scores are not published yet are re-checked on the next pass rather than queued.
| Key | Description | Default |
|---|---|---|
selected |
Torrent client to use. Currently only qbittorrent is supported. |
qbittorrent |
qbittorrent.host |
qBittorrent Web UI hostname or IP address. | localhost |
qbittorrent.port |
qBittorrent Web UI port. | 8080 |
qbittorrent.username |
Web UI username. | admin |
qbittorrent.password |
Web UI password. | adminadmin |
qbittorrent.add_paused |
Add torrents in paused state. | false |
qbittorrent.category |
Category tag applied to all gamarr-managed torrents. | games-gamarr |
| Key | Description | Default |
|---|---|---|
apprise_urls |
List of apprise service URLs. Leave empty to disable. | [] |
on_download |
Send a notification when a game is successfully added to qBittorrent. | true |
on_failure |
Send a notification when a game fails verification after max attempts. | false |
on_error |
Send a notification when an error occurs during the acquisition cycle. | false |
on_scrape_failure |
Send a notification when Metacritic scraping appears to be broken. | true |
| Key | Description | Default |
|---|---|---|
processed_expiry_days |
Delete processed history records older than this many days (currently unused — no automatic history pruning exists yet). | 365 |
| Key | Description | Default |
|---|---|---|
paths |
List of library root paths to scan when checking whether a game already exists. | [] |
Controls automatic copying of completed downloads to a game library. Mirrors movarr's post-processing pattern.
| Key | Description | Default |
|---|---|---|
post_process_enabled |
Master toggle for post-processing. | true |
schedule_time_mins |
Polling interval in minutes. | 5 |
run_on_start |
Run immediately on daemon start. | true |
library_path |
Destination path template. Supports {site}, {platform}, {genre}, {title}. Empty string disables copying. |
"" |
copy_completed |
Copy files to library after download completes. | true |
remove_completed |
Delete torrent + data after seeding goals are met. | true |
max_seed_wait_hours |
Fallback: delete after this many hours even if still seeding. 0 = never force-delete. |
168 |
exclude_file_min_kb |
Skip files smaller than this (0 = no minimum). | 0 |
exclude_file_regex_list |
Case-insensitive regex patterns to exclude files. | [] |
exclude_folder_regex_list |
Case-insensitive regex patterns to exclude folders. | [] |
path_case |
Controls casing of template variables. "pretty" uses display-friendly names for source (FitGirl, FreeGOG); "lowercase" downcases everything. Platform, genre, and title pass through unchanged (already correctly capitalized from Metacritic/user config). |
"pretty" |
Template variables in library_path:
| Variable | Source | Example |
|---|---|---|
{site} |
Download source name | fitgirl (lowercase) or FitGirl (pretty) |
{platform} |
Target platform | pc or PC |
{genre} |
First Metacritic genre | Action |
Casing of all variables is controlled by path_case above.
| {title} | Metacritic game title | Elden Ring |
Example: "/data/library/{site}/{platform}/{genre}/{title}" produces
"/data/library/FitGirl/pc/Action/Elden Ring" (with default path_case: "pretty").
flowchart TD
A([Start cycle]) --> B{Platform\nenabled?}
B -- No --> END([End])
B -- Yes --> C[Browse Metacritic\nconfigured sort_order]
C --> D[Parse browse-page\nscores & titles]
D --> E[For each game]
E --> F{Exclude\nkeyword\nin title?}
F -- Yes --> SKIP1([⛔ Skip])
F -- No --> G{Meets score\nthresholds on\nbrowse page?}
G -- No --> SKIP2([⛔ Skip])
G -- Yes --> H{Within max_date\nfloor / max_pages\nwindow?}
H -- No --> SKIP3([⛔ Skip])
H -- Yes --> K[Look up real scores\non Metacritic\ndetail page]
K --> M{Real scores\npass thresholds?}
M -- No --> SKIP4([⛔ Skip -\nre-checked next pass])
M -- Yes --> I[Add to pending queue,\nmarked verified]
I --> J[For each verified\npending game]
J --> L{Genre matches\nreject_genre?}
L -- Yes --> FAIL1([❌ Remove -\nrejected genre])
L -- No --> Q[Match verified\ngames against\nFitGirl sitemap]
Q --> R{FitGirl match\nfound?}
R -- No --> STAY([Stay in queue])
R -- Yes --> S[Fetch magnet link\nfrom FitGirl page]
S --> T[Add to qBittorrent\nwith gamarr-* tag]
T --> U([Record in\nhistory])
END --> V{Daemon\nmode?}
V -- Yes --> A
V -- No --> W([Exit])
- Metacritic browse — Scans Metacritic browse pages using the configured
sort_order;newtraverses the newest year first. Source matching and delivery use the same ordering, including games carried over from previous cycles. Important: browse pages show internal browse-only metrics, not the real 0–100 critic scores or 0–10 user scores. These rough scores only order and pre-filter candidates — the real threshold check happens before a game is queued (phase 3).mode: latestreads up tomax_pagespages of the window every cycle, newest year first with one page held back for the years below;mode: backlogsweeps back through it. Games are collected within themax_pagesbrowse window. Inbacklogdate mode (max_dateset withsort_order: new) the window is a month floor instead: the sweep stops inside the boundary year as soon as a page's games are all older thanmax_date, andmax_cycle_monthslimits how many months of backlog a single cycle covers —max_pagesandmax_cycle_pagesare then unused.mode: lateststill readsmax_pagespages per cycle whatever the date keys say. Forcriticscore/userscorethe date keys are ignored, because those browse lists mix years and no page corresponds to a date. - Browse-page filtering — Games whose titles match
reject_titleare skipped. In page mode, games outside themax_pageswindow are skipped,max_cycle_pagescontrols the per-cycle page limit (default0= unlimited), and the scanner advances through that window progressively — each cycle resumes where the last one left off, with auto-reset to page 1 when themax_pagesbudget is exhausted. In date mode the window and the pace come frommax_dateandmax_cycle_monthsinstead, both page keys are unused, and each cycle resumes from the per-year page cursors, restarting at the newest year once the floor has been swept. Note: browse scores are on a different scale and always exceed the configured thresholds — they cannot filter, so the threshold check happens on the detail page instead (phase 3). - Detail verification, then queueing — Each surviving game's real
Metacritic detail page is fetched and its 0–100 critic score and 0–10 user
score are compared against the configured thresholds. Only games that pass
are queued, and they enter
pending_gamesalready marked verified with their real scores, with the source-matching expiry window (download_sites.fitgirl.max_queue_days, default 60, or0for indefinite;review_sites.metacritic.platform_overrides.*.max_queue_daysis used only when no FitGirl source is configured). Games that fail — or whose scores are not published yet — are not queued; they are re-checked on the next browse pass. - Re-verification — Verified queue entries are re-checked against the
detail page each cycle. A game whose scores drop is not re-queued on the
old numbers: it stays in the queue only until a match attempt re-verifies it
and rejects it, or until it expires, and it is never delivered on stale data.
When scores first pass, the game's expiry is set to
now + <source>.max_queue_days(the FitGirl entry's value, default 60, or0for indefinite; another source's own value is used when no FitGirl source is configured). Later passing re-checks do not extend it, so a game that keeps passing but never matches still leaves the queue when the window lapses. - Genre rejection — Before score verification, the game's genres
(extracted from the detail page) are checked against
reject_genre. Matching games are removed from pending immediately — no score evaluation or re-verification (genres never change). - Source indexing — Only when there are verified games in the queue. Each enabled source's index is fetched (FreeGOG A-Z list, FitGirl sitemap) and new titles are stored for matching. The FreeGOG A-Z index stores each game as it is fetched, so a cycle aborted by the acquisition watchdog resumes where it left off next cycle instead of starting over — the progress line reports the true number of pages left.
- Source matching — Verified games are matched against the source index by title, best match first. A DLC/expansion title can match its base game's repack when the repack page confirms the included content; otherwise a fuzzy candidate that merely shares title words (e.g. "2", "master", "collection") is accepted only when the repack page actually names the game, so an unrelated repack is no longer picked on URL order alone. The matched title gets its magnet link fetched (pre-stored for FreeGOG, on-demand for FitGirl).
- Delivery — Matched games are added to qBittorrent with a
gamarr-*tag. A magnet qBittorrent already holds is never re-uploaded: one info line reports it. A torrent gamarr owns (one it tagged earlier, or one sitting in gamarr's category — that is the opt-in, even for a torrent you added) is adopted so post-processing still copies it, while a torrent outside gamarr's category is recorded as skipped, since there is nothing for gamarr to copy. If the torrent cannot be confirmed or adopted at all, the game stays pending and is retried next cycle. The result is recorded in the history database. - Notifications — Optional Apprise notifications on download, failure, or error.
The codebase is structured as a Metacritic-first pipeline:
metacritic.py → pipeline.py → sources/freegog.py → qbittorrent.py
sources/fitgirl.py → post_processor.py
_(Note: `sources/` is the Python package name for download site implementations — distinct from the config key.)
↓ ↓ ↓ ↓
Browse new Score filter + Sitemap match Add torrent
releases pending queue + magnet fetch post_processor.py
↓
Copy to library
+ SHA-256 verify
+ cleanup
All configuration is driven by a YAML file (configs/gamarr.yml) validated
with Pydantic. The scheduler (scheduler.py) runs in single-pass mode by
default, or in continuous scheduled mode when
schedule.enabled is true.
git clone https://github.com/binhex/gamarr
cd gamarr
uv venv --quiet
uv sync --extra devBefore committing, run the full lint suite:
pre-commit run --all-filesuv run pytestQ: What download sites are supported?
A: FreeGOGPCGames and FitGirl repacks.
Q: Can I add Nintendo Switch games?
A: Planned for a future release via Jackett/Prowlarr integration.
Q: What if qBittorrent is not running?
A: gamarr will log a warning and skip the acquisition cycle.
Q: How do I reject specific game genres?
A: Use the reject_genre option under review_sites.metacritic.platform_overrides.pc.
It uses case-insensitive substring matching. For example:
["RPG"]matches "Action RPG", "Western RPG", "JRPG", "RPG"["Western RPG"]only matches genres containing "Western RPG"["Action"]matches "Action", "Action RPG", "Open-World Action"
See the genres section for details.
Q: Why do I see "X from previous cycles" in the log — why aren't those games leaving the queue?
A: Games leave the pending queue in only two ways:
- Downloaded — scores pass thresholds AND a matching torrent is found on a source
- Expired — the game has been in the queue longer than
max_queue_days
Games that pass score thresholds but have no source match stay in the queue — they get re-verified each cycle because a new FitGirl/FreeGOG repack could appear. If you find the queue growing indefinitely:
- Increase
max_pagesto catch more recently released games - Or set
max_datewithsort_order: newto state the oldest month you want reviewed, so the cutoff is a date instead of a page count - Games that fail score thresholds are not queued at all — they are re-checked when the next browse pass reaches their pages again
- The message
0 new + X from previous cyclesmeans browsing found nothing new
Q: Why does max_queue_days have two separate settings — one for Metacritic and one for FitGirl?
A: There are two waiting rooms. When a game is first discovered, it sits
in the first waiting room while gamarr checks its Metacritic scores. The
review_sites.metacritic max_queue_days is the time limit for this phase — if scores
can't be verified in time, the game is removed.
Once scores pass, the game moves to a second waiting room where it waits for
a source match (e.g. FitGirl repack). The per-source max_queue_days
(e.g. fitgirl.max_queue_days) starts a
fresh countdown from this point — it doesn't matter how long the game
spent in the first room. This gives the game a full window to find a match,
regardless of how long the score-checking took.
So even if both values are the same (say 30 days each), a game that took 25 days to get scores verified still gets a full 30 days to find a source match — not just 5.
If you appreciate my work, then please consider buying me a beer :D
