Keep your Radarr/Sonarr library aligned with your quality profiles, automatically and efficiently. Optimizarr continuously re-evaluates the releases available for every movie and episode you already have, and grabs a better one when it exists: a genuine quality upgrade, or a leaner file at the same quality. It picks the way a careful human would, weighing score, resolution, and size together against the profile you chose, instead of assuming bigger is better.
It does two jobs, both optional and independent:
- Optimizer: a continuous worker that improves the files you already have.
- Unmonitor: a cron job that stops the *arr apps from chasing pointless upgrades forever.
- Realign an existing library to a profile, including downgrades. Change a Radarr/Sonarr quality profile and the app will not touch the files you already have. Optimizarr will: point a movie at a leaner profile and it will shrink the file, lower the bitrate, even drop resolution to match what you now want. Radarr/Sonarr only ever upgrade; Optimizarr realigns in both directions. This is the most-requested thing it does.
- Reclaim disk. The size-leaning profiles (Efficient, Compact) swap bloated files for good-but-smaller encodes, without ever making a file bigger unless it is a real upgrade.
- Pick like a human, per profile. Five shipped profiles (Remux, Quality, Balanced, Efficient, Compact) each have their own taste, from "max quality, size be damned" to "smallest file that is still good." A multi-objective ranking (TOPSIS) plus hard size and resolution gates chooses among the releases your indexers actually offer.
- Stop the endless upgrade chase. The Unmonitor job unmonitors movies/episodes a set number of days after release, so Radarr/Sonarr stop grabbing "new" releases off RSS just because they appeared.
It is safe by design: it never inflates a file to hit a target, it cannot oscillate (every swap strictly raises a single quality score, so a file is never revisited), and "optimized" means the algorithm can no longer find anything better, never merely "we triggered a grab" (grabs fail to download all the time, and it handles that).
Want the details: the per-preset size tables, the swap rule, the TOPSIS formulas, the config model, and the worker loop? See ALGORITHM.md.
This tool was developed entirely with AI (Claude Opus). I am an experienced developer, I have reviewed all of the code, and it is thoroughly tested with good unit-test coverage. I would not have released it if I were not satisfied with its quality. That said, I think it is important to disclose when AI is used to build tools.
Optimizarr pairs with Profilarr. The
defaults assume Profilarr's custom-format scoring convention, where a "perfect" release scores
around 1,000,000, and the shipped profiles auto-attach to a Radarr/Sonarr quality profile
whose name contains a keyword, e.g. 2160p Quality to Quality, 1080p Efficient to Efficient,
2160p Remux to Remux.
It is not strictly required. Both the score scale and the profiles are configurable, so Optimizarr works against any custom-format scoring scheme. Profilarr just saves you the tuning.
services:
optimizarr:
image: ghcr.io/patrix87/optimizarr:latest
container_name: optimizarr
restart: unless-stopped
environment:
TZ: America/Toronto
LOG_LEVEL: INFO
RADARR_URL: http://radarr:7878
RADARR_API_KEY: ${RADARR_API_KEY}
SONARR_URL: http://sonarr:8989
SONARR_API_KEY: ${SONARR_API_KEY}
volumes:
- ./config:/config # put config.toml here
- ./data:/data # state.json lives hereDrop a .env next to it with RADARR_API_KEY=... / SONARR_API_KEY=..., put your
config.toml in ./config/, then docker compose up -d. Pin a tag (e.g. :v0.2.0) instead of
:latest for reproducible deploys; to build from a clone, swap image: for build: ..
Try it dry first: set dry_run = true in config.toml to log every would-be grab/unmonitor
without changing anything.
Enable Failed Download Handling in Radarr/Sonarr (it is on by default). The optimizer relies on it to blocklist dead releases so it can move to the next-best one.
Two parts:
- Environment holds only secrets, URLs, and paths.
config.tomlholds all behavior, layered on top of the bundleddefaults.toml, so you only set what you want to change.defaults.tomldocuments every option inline: read it, then copy just the keys you want to change into yourconfig.toml. With noconfig.tomlat all, the defaults are used as-is.
| Variable | Default | Notes |
|---|---|---|
RADARR_URL / RADARR_API_KEY |
unset | Set both to enable Radarr. |
SONARR_URL / SONARR_API_KEY |
unset | Set both to enable Sonarr. |
LOG_LEVEL |
INFO |
DEBUG, INFO, WARNING, ERROR. |
TZ |
container default | Affects cron evaluation and log timestamps. |
If neither Radarr nor Sonarr is configured, the container exits 1.
The defaults are sensible; most setups only adjust a handful of keys (every key is documented
inline in defaults.toml):
| Key | What it does |
|---|---|
[optimizer] enabled / [unmonitor] enabled |
Turn each feature on/off independently. |
[optimizer.<app>] min_age_days + release_type |
Only touch items this many days past every listed date, so freshly released/imported items are left alone. |
[optimizer.<app>] allow_size_increase |
false blocks any bigger file (also blocks resolution upgrades). |
[optimizer.<app>] allow_quality_downgrade |
false blocks lower-score releases. Turn this off if you only ever want upgrades. Leaving it on is what lets Efficient/Compact realign downward. |
[unmonitor.<app>] days / release_type / require_cutoff_met |
When to unmonitor after release, and whether to wait for the quality cutoff first. |
[optimizer.topsis] |
The selection engine: per-preset size tables (floor/target/bloat), presets, and tuning. You rarely need this. See ALGORITHM.md. |
The optimizer selects items by hasFile, regardless of monitored state. It improves the
existing library, and the unmonitor job deliberately strips monitoring once a file exists.
uv sync
uv run pytest
uv run ruff check .
uv run ruff format --check .
# Run against a real Radarr/Sonarr without Docker:
cp .env.example .env # fill in URLs + API keys
# Optionally create a config.toml with any overrides (it layers on optimizarr/defaults.toml,
# which documents every option). With no config.toml, the built-in defaults are used.
# optimizarr reads /config/config.toml and writes /data/state.json by default;
# to run outside Docker, edit CONFIG_PATH / STATE_PATH at the top of optimizarr/config.py.
uv run --env-file .env python -m optimizarrSet dry_run = true in config.toml first to log would-be actions without writing anything (no
grabs, no unmonitors, no state changes). tools/weight_lab.py renders how each profile scores and
picks on sample releases.
Two workflows live in .github/workflows/:
- ci.yml runs
ruff check,ruff format --check, andpyteston every push tomainand every pull request. - release.yml builds a
linux/amd64image and pushes it toghcr.io/patrix87/optimizarr(the image name follows the repo via${{ github.repository }}), then publishes a GitHub Release with auto-generated notes for the tag.
| Trigger | Image tags pushed | GitHub Release |
|---|---|---|
Push tag v1.2.3 |
1.2.3, 1.2, 1, latest |
created (auto notes) |
Manual workflow_dispatch |
(none, no semver tag applies) | not created |
git tag v0.2.0
git push --tagsOne-time repo setup before the first release run:
- Settings, Actions, General, Workflow permissions: Read and write permissions.
- After the first push, open the package at
https://github.com/patrix87?tab=packages, set visibility to Public, and confirm it linked to the repo.
uv lock --upgradePolyForm Noncommercial License 1.0.0. You may use, modify, and share it for any noncommercial purpose, and you must keep the required attribution notice. Commercial use is not permitted without a separate agreement. Copyright (c) 2026 Patrick Veilleux (Patrix87).