A webhook service that "grooms" rough releases into presentable ones — automatically renaming torrents in qBittorrent when Sonarr or Radarr grabs a release.
Fixes infinite download loops (Couldn't add release X from Indexer Y to download queue.) in Sonarr and Radarr by renaming torrents to match their original release titles, ensuring that metadata (Custom Formats, Release Groups) lost during filename parsing is preserved.
- Web UI: Manage all rename rules from a modern dashboard — no YAML editing required. Edit global and per-tracker rules, simulate how a release would be filtered and renamed (live), and apply changes safely.
- Operations dashboard: A searchable, filterable audit log of every received webhook, decision, and rename. Drill into any operation for a clear before → after of the torrent/folder/files, see its current live state in qBittorrent, and safely roll back a rename (verified, no data loss).
- Receives webhooks from Sonarr/Radarr on Grab events
- Renames torrents, folders, and files in qBittorrent
- Configurable trigger filters (indexer, quality, custom formats, etc.)
- Customizable rename rules (regex patterns, prefix/suffix)
- Multiple rename modes (torrent only, folder, files)
- Handles timing issues with automatic retry/polling
- Score validation: Optionally validates renames against Sonarr/Radarr API to ensure custom format scores aren't negatively impacted
Open http://<host>:8000/ in a browser to manage Groomarr without touching YAML. The Dashboard is the default landing tab.
-
Dashboard — A live overview plus a searchable, filterable log of every operation: each received webhook, the trigger decision (processed vs. skipped, and why), which rename rule changed the name, and the resulting torrent/folder/file renames. KPI cards summarise activity (renamed / skipped / failed / rolled back) and double as quick filters. Open any entry to see:
- the full per-filter trigger breakdown and step-by-step rename rule trace,
- a clear before → after of the torrent name, root folder, and every file (with correct per-season episode numbers on multi-season packs),
- the torrent's current live state in qBittorrent — its download status (downloading %, completed, seeding, paused), whether it still exists, and whether it still carries the name Groomarr gave it (drift detection, shown only for renames), plus a one-click jump to Tools with the hash and name pre-filled to preview or re-run a manual rename, and
- a one-click rollback that safely reverses the rename. Rollback is verified against live state and runs the same data-loss safety gate as a normal rename: anything that changed since is skipped rather than overwritten, so a rollback never loses a file. The history is stored in a small SQLite database on the
/configvolume.
-
Rules — A single guided workspace: edit the global rule set and per-tracker overrides (trigger filters and rename rules), with inline help on every field, while a live preview on the right reacts to every change before you save. For an editable test release it shows, in real time:
- which tracker matched (or global) and whether the release would be processed,
- a per-filter breakdown explaining why — including non-title rules like excludes, download client, custom formats, and score,
- the resulting torrent/folder name with a step-by-step transformation, and
- how the individual files would be renamed.
You can load a real torrent into the preview by hash or tracker ID. Regex patterns are validated as you type. Saving writes
rename_rules.yaml(backing up the previous file to.bak) and reloads automatically. -
Tools — Preview a rename, apply a manual rename, or find a torrent by its tracker ID.
-
Status — Connectivity (qBittorrent/Sonarr/Radarr) and the current deployment settings.
Warning
Groomarr has no built-in authentication. Anyone who can reach the port can change rules and trigger renames. Keep it on a trusted/private network or behind an authenticating reverse proxy. Do not expose port 8000 directly to the internet. Set CONFIG_READONLY=true to make the UI/API unable to modify rules.
# Copy example config
cp config/rename_rules.yaml.example config/rename_rules.yaml
# Edit config (optional - defaults work out of the box)
nano config/rename_rules.yaml# Edit docker-compose.yml with your qBittorrent credentials
docker-compose up -dIn Radarr:
- Go to Settings → Connect → + → Webhook
- Name:
Groomarr - On Grab: ✓ (enable)
- URL:
http://groomarr:8000/webhook/radarr - Method: POST
- Click Test, then Save
In Sonarr:
- Go to Settings → Connect → + → Webhook
- Name:
Groomarr - On Grab: ✓ (enable)
- URL:
http://groomarr:8000/webhook/sonarr - Method: POST
- Click Test, then Save
| Variable | Default | Description |
|---|---|---|
QBITTORRENT_URL |
http://qbittorrent:8080 |
qBittorrent Web UI URL |
QBITTORRENT_USERNAME |
admin |
qBittorrent username |
QBITTORRENT_PASSWORD |
adminadmin |
qBittorrent password |
API_PORT |
8000 |
Port for webhook API |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
LOG_FORMAT |
text |
Log format: text or json (for log aggregation) |
RENAME_MODE |
torrent_and_folder |
What to rename (see below) |
DRY_RUN |
false |
If true, logs what would be renamed without making changes |
INITIAL_DELAY |
2 |
Seconds to wait before first torrent lookup |
MAX_RETRIES |
10 |
Max attempts to find torrent |
RETRY_DELAY |
3 |
Base seconds between retries (uses exponential backoff) |
SONARR_URL |
null |
Sonarr API URL (for score validation) |
SONARR_API_KEY |
null |
Sonarr API key (Settings → General) |
RADARR_URL |
null |
Radarr API URL (for score validation) |
RADARR_API_KEY |
null |
Radarr API key (Settings → General) |
STATIC_DIR |
frontend/dist |
Directory of the built web UI (set automatically in the Docker image) |
CONFIG_READONLY |
false |
If true, the web UI/API cannot modify rename_rules.yaml |
HISTORY_DB |
(next to rules file) | Path to the operation-history SQLite database. Defaults to groomarr_history.db beside rename_rules.yaml, so the same /config volume persists it across restarts. |
HISTORY_RETENTION_DAYS |
0 |
Delete operation-history rows older than N days (0 = keep forever). |
HISTORY_RETENTION_MAX_ROWS |
10000 |
Keep at most N most-recent operation-history rows (0 = unlimited). |
| Mode | Description |
|---|---|
torrent_only |
Only rename torrent display name in qBittorrent UI |
torrent_and_folder |
Rename torrent name + root folder (default) |
torrent_folder_files |
Rename torrent + folder + all files |
folder_only |
Only rename root folder |
files_only |
Only rename files |
Create config/rename_rules.yaml to customize behavior. The configuration supports two formats:
- Legacy flat format: All rules at root level (backward compatible)
- Hierarchical format:
globalsection +trackerslist for per-indexer rules
When a webhook is received:
- Check if the indexer matches any tracker in the
trackerslist (first match wins) - If matched → use that tracker's rules exclusively (global rules are NOT applied)
- If no match → use global rules
# Global rules - used when no tracker-specific config matches
global:
# Trigger filters
indexers_exclude:
- ".*Public.*"
qualities_exclude:
- "CAM"
- "TS"
# Rename rules
prefix: ""
suffix: ""
# Score validation
validate_custom_format_score: false
score_validation_policy: "block"
# Tracker-specific rules - first matching tracker wins
trackers:
- name: "my-private-tracker"
match:
- "MyPrivateTracker (API)" # Exact match (case-insensitive)
- "MyPrivateTracker*" # Wildcard match
rules:
qualities_include:
- "Bluray.*"
- "Remux"
prefix: "[MPT] "
validate_custom_format_score: true
- name: "anime-tracker"
match:
- "Nyaa*"
- "AnimeBytes*"
- "/.*anime.*/i" # Regex match (wrapped in slashes)
rules:
release_groups_include:
- "SubsPlease"
- "Erai-raws"
remove_patterns:
- "\\[.*?\\]" # Remove [tags] common in anime
suffix: " [Anime]"| Pattern | Example | Matches |
|---|---|---|
| Exact string | "TrackerName" |
Case-insensitive exact match |
| Wildcard | "Tracker*", "*Cinema*" |
Shell-style glob (* = any chars, ? = single char) |
| Regex | "/Tracker.*API/" |
Regular expression (wrapped in slashes) |
For simpler setups without tracker-specific rules:
# All rules at root level (no 'global:' or 'trackers:' sections)
indexers_include:
- "TrackerA.*"
- "IndexerB"
indexers_exclude:
- ".*Public.*"
qualities_exclude:
- "CAM"
- "TS"
prefix: ""
suffix: ""
validate_custom_format_score: false
score_validation_policy: "block"| Filter | Description |
|---|---|
indexers_include |
Only process these indexers (regex) |
indexers_exclude |
Skip these indexers |
qualities_include |
Only process these qualities |
qualities_exclude |
Skip these qualities |
customformats_require_any |
Require any of these custom formats |
customformats_exclude |
Skip if any of these present |
min_customformat_score |
Minimum score threshold (null = disabled) |
download_clients_include |
Only process from these clients |
download_clients_exclude |
Skip these download clients |
release_groups_include |
Only process these release groups |
release_groups_exclude |
Skip these release groups |
| Rule | Description |
|---|---|
prefix |
Add prefix to renamed titles |
suffix |
Add suffix to renamed titles |
remove_patterns |
Regex patterns to remove from title |
replace_patterns |
Pattern → replacement mapping |
skip_title_patterns |
Skip renaming if title matches these |
Score validation is an optional feature that uses the Sonarr/Radarr API to compare custom format scores before and after renaming. This helps ensure that your rename rules don't accidentally remove information that Sonarr/Radarr uses for matching.
How it works:
- When a rename is triggered, Groomarr calls the
/api/v3/parseendpoint with both the original and new name - It compares the
customFormatScorevalues returned by the API - Based on the policy, it either blocks or warns if the new name would have a lower score
Configuration:
- Set the environment variables for Sonarr/Radarr API access
- Enable in
rename_rules.yaml:
validate_custom_format_score: true
score_validation_policy: "block" # or "warn"Policies:
block(default): Skip the rename if the score would decreasewarn: Log a warning but proceed with the rename anyway
Example log output:
# Score validation passed
[radarr] Score validation: 'Original.Name' (11200) -> 'New.Name' (11200), change=0, safe=True
# Score decrease blocked
[radarr] Skipping rename: score would decrease from 11200 to 8500 (-2700)
# API unreachable
[radarr] Skipping rename: Radarr API unreachable at http://radarr:7878
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Web UI (single-page app) |
/health |
GET | Health check |
/webhook/radarr |
POST | Radarr webhook receiver |
/webhook/sonarr |
POST | Sonarr webhook receiver |
/rename/manual |
POST | Manually rename a torrent by hash |
/rename/preview |
POST | Preview what a rename operation would do without making changes |
/find/torrent |
POST | Find a torrent by its tracker ID (URL or number) |
/reload |
GET | Reload rename rules |
/api/config |
GET / PUT | Read or save rename rules (used by the web UI) |
/api/rules/simulate |
POST | Simulate filtering + renaming for a sample release (trigger breakdown + file renames) |
/api/rules/validate-pattern |
POST | Validate a regex or indexer match pattern |
/api/torrent-sample |
POST | Load a real torrent's name + files into the preview (by hash or tracker ID) |
/api/settings |
GET | Non-sensitive runtime settings |
/api/status |
GET | Service + connectivity status |
/docs |
GET | Swagger API documentation |
The /rename/manual endpoint allows direct renaming of torrents without a webhook event:
curl -X POST http://localhost:8000/rename/manual \
-H "Content-Type: application/json" \
-d '{
"torrent_hash": "AF35BC0E03A9D8405779A69FC9A438F1BFE90C5F",
"new_name": "Movie.2024.1080p.BluRay.x264-GROUP",
"mode": "torrent_and_folder"
}'Parameters:
torrent_hash(required): The torrent info hashnew_name(required): New name to applymode(optional): Rename mode (default:torrent_and_folder)
The /rename/preview endpoint shows exactly what would happen if you performed a rename operation without actually making any changes. This is useful for testing rename rules or verifying behavior before committing to a rename.
curl -X POST http://localhost:8000/rename/preview \
-H "Content-Type: application/json" \
-d '{
"torrent_hash": "AF35BC0E03A9D8405779A69FC9A438F1BFE90C5F",
"new_name": "Movie.2024.1080p.BluRay.x264-GROUP",
"mode": "torrent_folder_files"
}'Parameters:
torrent_hash(required): The torrent info hashnew_name(required): New name to previewmode(optional): Rename mode to preview (default:torrent_and_folder)
Response includes:
- Current state: torrent name, root folder, total files
- Proposed changes: new torrent name, new folder name, list of file renames
- Change indicators: which items will actually change
- Warnings: any issues detected (e.g., conflicts, missing folders)
This endpoint is read-only and never modifies your torrents, making it safe to use for testing and validation.
services:
groomarr:
image: maksii/groomarr:latest
container_name: groomarr
environment:
- QBITTORRENT_URL=http://qbittorrent:8080
- QBITTORRENT_USERNAME=admin
- QBITTORRENT_PASSWORD=your_password
- RENAME_MODE=torrent_and_folder
# Optional: Enable score validation (requires Arr API access)
# - SONARR_URL=http://sonarr:8989
# - SONARR_API_KEY=your-sonarr-api-key
# - RADARR_URL=http://radarr:7878
# - RADARR_API_KEY=your-radarr-api-key
volumes:
- ./config:/config
ports:
- "8000:8000"
restart: unless-stopped
networks:
- media # Same network as qBittorrent, Sonarr, Radarr
networks:
media:
external: true- Sonarr/Radarr grabs a release and sends webhook to this service
- Service validates the webhook and applies trigger filters
- If filters pass, a background task is queued
- Background task waits for torrent to appear in qBittorrent (with retries)
- Rename rules are applied to generate new name
- Torrent/folder/files are renamed based on configured mode
flowchart LR
subgraph arr [Sonarr/Radarr]
A[Grab Release]
end
subgraph groomarr [Groomarr]
B[Receive Webhook]
C{Validate & Filter}
D[Queue Task]
E[Return 200]
end
subgraph background [Background Task]
F[Wait for Torrent]
G[Apply Rename Rules]
H[Rename in qBit]
end
subgraph qbit [qBittorrent]
I[Torrent Renamed]
end
A -->|POST webhook| B
B --> C
C -->|Pass| D
C -->|Fail| E
D --> E
D -.->|async| F
F --> G
G --> H
H --> I
- Increase
MAX_RETRIESorRETRY_DELAY - Check qBittorrent is accessible from the container
- Verify
QBITTORRENT_URLis correct
- Check Sonarr/Radarr can reach the service URL
- Verify firewall/network settings
- Check logs:
docker logs groomarr
- All filter patterns are regex (case-insensitive)
- Use
docker logsto see skip reasons - Test patterns at https://regex101.com
docker logs -f groomarr# Install backend dependencies
pip install -r requirements.txt
# Build the web UI (served by FastAPI at /)
cd frontend && npm ci && npm run build && cd ..
# Run the backend
python -m uvicorn src.main:app --reload --port 8000Then open http://localhost:8000/.
For a fast frontend dev loop with hot-reload, run the Vite dev server alongside the backend. It proxies API/webhook requests to http://localhost:8000:
# Terminal 1 — backend
python -m uvicorn src.main:app --reload --port 8000
# Terminal 2 — frontend dev server (http://localhost:5173)
cd frontend
npm install
npm run devBuild a production bundle with npm run build (runs tsc type-checking, then Vite). The Docker image builds this automatically in a multi-stage build.
# Backend
pip install pytest
pytest tests/ -v
# Frontend type-check + build
cd frontend && npm run build- qBittorrent v4.2.1+ (for file renaming support)
- Sonarr v3+ / Radarr v3+
- Docker (recommended)
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Run the tests (
pytest tests/ -v) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT